$idFields DTO -> entity ID field mapping
* @param class-string $entityClass
*/
#[HasNamedArguments]
public function __construct(
public string $entityClass,
public string $errorPath,
public array $fields,
public array $idFields = [],
) {
parent::__construct();
if (0 === \count($fields)) {
throw new InvalidOptionsException('`fields` option must have at least one field', ['fields']);
}
if (null === $entityClass || '' === $entityClass) {
throw new InvalidOptionsException('Bad entity class', ['entityClass']);
}
}
public function getTargets(): array
{
return [Constraint::CLASS_CONSTRAINT];
}
}
================================================
FILE: src/Validator/UniqueValidator.php
================================================
entityManager->createQueryBuilder()
->select('COUNT(e)')
->from($constraint->entityClass, 'e');
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($constraint->fields as $dtoField => $entityField) {
if (\is_int($dtoField)) {
$dtoField = $entityField;
}
$fieldValue = $propertyAccessor->getValue($value, $dtoField);
if (\is_string($fieldValue)) {
$qb->andWhere($qb->expr()->eq("LOWER(e.$entityField)", ":f_$entityField"));
$qb->setParameter("f_$entityField", mb_strtolower($fieldValue));
} else {
$qb->andWhere($qb->expr()->eq("e.$entityField", ":f_$entityField"));
$qb->setParameter("f_$entityField", $fieldValue);
}
}
foreach ($constraint->idFields as $dtoField => $entityField) {
if (\is_int($dtoField)) {
$dtoField = $entityField;
}
$fieldValue = $propertyAccessor->getValue($value, $dtoField);
if (null !== $fieldValue) {
$qb->andWhere($qb->expr()->neq("e.$entityField", ":i_$entityField"));
$qb->setParameter("i_$entityField", $fieldValue);
}
}
$count = $qb->getQuery()->getSingleScalarResult();
if ($count > 0) {
$this->context->buildViolation($constraint->message)
->setCode(Unique::NOT_UNIQUE_ERROR)
->atPath($constraint->errorPath)
->addViolation();
}
}
}
================================================
FILE: templates/_email/application_approved.html.twig
================================================
{% extends '_email/email_base.html.twig' %}
{%- block title -%}
{{- 'email_application_approved_title'|trans }}
{%- endblock -%}
{% block body %}
{{ 'email_application_approved_body'|trans({
'%link%': url('app_login'),
'%siteName%': kbin_domain(),
})|raw }}
{% if user.isVerified is same as false %}
{{ 'email_verification_pending'|trans }}
{% endif %}
{% endblock %}
================================================
FILE: templates/_email/application_rejected.html.twig
================================================
{% extends '_email/email_base.html.twig' %}
{%- block title -%}
{{- 'email_application_rejected_title'|trans }}
{%- endblock -%}
{% block body %}
{{ 'email_application_rejected_body'|trans }}
{% endblock %}
================================================
FILE: templates/_email/confirmation_email.html.twig
================================================
{% extends "_email/email_base.html.twig" %}
{%- block title -%}
{{- 'email_confirm_header'|trans }}
{%- endblock -%}
{% block body %}
{{ 'email_confirm_header'|trans }}
{{ 'email_confirm_content'|trans }}
{{ 'email_verify'|trans }}
{% if user.getApplicationStatus() is not same as enum('App\\Enums\\EApplicationStatus').Approved %}
{{ 'email_application_pending'|trans }}
{% endif %}
{{ 'email_confirm_expire'|trans }}
Cheers!
{% endblock %}
================================================
FILE: templates/_email/contact.html.twig
================================================
{% extends "_email/email_base.html.twig" %}
{%- block title -%}
{{- 'contact'|trans }}
{%- endblock -%}
{% block body %}
{{ 'contact'|trans }}
Name: {{ name }}
Email: {{ senderEmail }}
Message: {{ message }}
{% endblock %}
================================================
FILE: templates/_email/delete_account_request.html.twig
================================================
{% extends "_email/email_base.html.twig" %}
{%- block title -%}
{{- 'email.delete.title'|trans }}
{%- endblock -%}
{% block body %}
{{'email.delete.title'|trans}}
{{'email.delete.description'|trans}}
Username: {{ username }}
Email: {{ mail }}
{% endblock %}
================================================
FILE: templates/_email/email_base.html.twig
================================================
{% apply inline_css(encore_entry_css_source('email')) %}
{%- block title -%}{{ kbin_meta_title() }}{%- endblock -%}
{% block body %}{% endblock %}
{% endapply %}
================================================
FILE: templates/_email/reset_pass_confirm.html.twig
================================================
{% extends "_email/email_base.html.twig" %}
{%- block title -%}
{{- 'password_confirm_header'|trans }}
{%- endblock -%}
{% block body %}
{{ 'password_confirm_header'|trans }}
{{ 'email_confirm_expire'|trans }}
{{'email_confirm_button_text'|trans}}
Cheers!
{{'email_confirm_link_help'|trans}}: {{ url('app_reset_password', {token: resetToken.token}) }}
{% endblock %}
================================================
FILE: templates/admin/_options.html.twig
================================================
{%- set TYPE_GENERAL = constant('App\\Repository\\StatsRepository::TYPE_GENERAL') -%}
{%- set TYPE_CONTENT = constant('App\\Repository\\StatsRepository::TYPE_CONTENT') -%}
{%- set TYPE_VOTES = constant('App\\Repository\\StatsRepository::TYPE_VOTES') -%}
{%- set STATUS_PENDING = constant('App\\Entity\\Report::STATUS_PENDING') -%}
================================================
FILE: templates/admin/dashboard.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'dashboard'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-dashboard{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% include 'stats/_filters.html.twig' %}
{{ 'users'|trans|upper }}
{{ users }}
{{ 'magazines'|trans|upper }}
{{ magazines }}
{{ 'votes'|trans|upper }}
{{ votes }}
{{ 'threads'|trans|upper }}
{{ entries }}
{{ 'comments'|trans|upper }}
{{ comments }}
{{ 'posts'|trans|upper }}
{{ posts }}
{% endblock %}
================================================
FILE: templates/admin/deletion_magazines.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'deletion'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-deletion{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if magazines|length %}
{{ 'name'|trans }}
{{ 'threads'|trans }}
{{ 'comments'|trans }}
{{ 'posts'|trans }}
{{ 'marked_for_deletion'|trans }}
{% for magazine in magazines %}
{{ component('magazine_inline', { magazine: magazine, stretchedLink: true, showAvatar: true, showNewIcon: true}) }}
{{ magazine.entryCount }}
{{ magazine.entryCommentCount }}
{{ magazine.postCount + magazine.postCommentCount }}
{{ component('date', {date: magazine.markedForDeletionAt}) }}
{% endfor %}
{% else %}
{% endif %}
{% if(magazines.haveToPaginate is defined and magazines.haveToPaginate) %}
{{ pagerfanta(magazines, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/deletion_users.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'deletion'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-deletion{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if users|length %}
{{ 'username'|trans }}
{{ 'email'|trans }}
{{ 'created_at'|trans }}
{{ 'marked_for_deletion'|trans }}
{% for user in users %}
{{ component('user_inline', {user: user, showNewIcon: true}) }}
{{ user.email }}
{{ component('date', {date: user.createdAt}) }}
{{ component('date', {date: user.markedForDeletionAt}) }}
{% endfor %}
{% else %}
{% endif %}
{% if(users.haveToPaginate is defined and users.haveToPaginate) %}
{{ pagerfanta(users, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/federation.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'federation'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-federation page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{{ form_start(form) }}
{{ form_label(form.federationEnabled, 'federation_enabled') }}
{{ form_widget(form.federationEnabled) }}
{{ form_label(form.federationPageEnabled, 'federation_page_enabled') }}
{{ form_widget(form.federationPageEnabled) }}
{{ form_label(form.federationUsesAllowList, 'federation_uses_allowlist') }}
{{ form_widget(form.federationUsesAllowList) }}
{{ form_help(form.federationUsesAllowList) }}
{{ form_row(form.submit, { 'label': 'save'|trans, attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% if useAllowList %}
{% else %}
{% endif %}
{% if useAllowList %}
{{ 'allowed_instances'|trans }}
{% else %}
{{ 'banned_instances'|trans }}
{% endif %}
{{ component('instance_list', {'instances': instances, 'showDenyButton': useAllowList, 'showUnBanButton': not useAllowList}) }}
{{ 'instances'|trans }}
{{ component('instance_list', {
'instances': allInstances,
'showDenyButton': useAllowList,
'showUnBanButton': not useAllowList,
'showAllowButton': useAllowList,
'showBanButton': not useAllowList
}) }}
{% endblock %}
================================================
FILE: templates/admin/federation_defederate_instance.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'defederating_instance'|trans }} {{ instance.domain }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-federation page-defederation{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% include 'layout/_flash.html.twig' %}
{{ 'defederating_instance'|trans({'%i': instance.domain}) }}
{{ 'magazines'|trans }}
{{ counts['magazines'] }}
{{ 'users'|trans }}
{{ counts['users'] }}
{{ 'their_user_follows'|trans }}
{{ counts['ourUserFollows'] }}
{{ 'our_user_follows'|trans }}
{{ counts['theirUserFollows'] }}
{{ 'their_magazine_subscriptions'|trans }}
{{ counts['ourSubscriptions'] }}
{{ 'our_magazine_subscriptions'|trans }}
{{ counts['theirSubscriptions'] }}
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_errors(form.confirm) }}
{{ form_label(form.confirm, 'confirm_defederation') }}
{{ form_widget(form.confirm) }}
{{ form_row(form.submit, { 'label': useAllowList ? 'btn_deny'|trans : 'ban'|trans, attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/admin/magazine_ownership.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'ownership_requests'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-ownership-requests{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if requests|length %}
{{ 'magazine'|trans }}
{{ 'user'|trans }}
{{ 'reputation_points'|trans }}
{{ 'action'|trans }}
{% for request in requests %}
{{ component('magazine_inline', {magazine: request.magazine, showNewIcon: true}) }}
{{ component('user_inline', {user: request.user, showNewIcon: true}) }}
{{ get_reputation_total(request.user) }}
{% endfor %}
{% else %}
{% endif %}
{% if(requests.haveToPaginate is defined and requests.haveToPaginate) %}
{{ pagerfanta(requests, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/moderators.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderators'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-moderators{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if moderators|length %}
{% for moderator in moderators %}
{% if moderator.avatar %}
{{ component('user_avatar', {user: moderator}) }}
{% endif %}
{% endfor %}
{% if(moderators.haveToPaginate is defined and moderators.haveToPaginate) %}
{{ pagerfanta(moderators, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
{{ form_start(form) }}
{{ form_errors(form.user) }}
{{ form_label(form.user, 'username') }}
{{ form_widget(form.user) }}
{{ form_row(form.submit, { 'label': 'add_moderator', attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/admin/monitoring/_monitoring_single_options.html.twig
================================================
================================================
FILE: templates/admin/monitoring/_monitoring_single_overview.html.twig
================================================
{% set queryDuration = context.queryDurationMilliseconds %}
{% set twigDuration = context.twigRenderDurationMilliseconds %}
{% set curlRequestDuration = context.curlRequestDurationMilliseconds %}
{{ 'monitoring_user_type'|trans }} {{ context.userType }}
{{ 'monitoring_path'|trans }} {{ context.path }}
{{ 'monitoring_handler'|trans }} {{ context.handler }}
{{ 'monitoring_started'|trans }} {{ component('date', {'date': context.startedAt}) }}
{{ 'monitoring_duration'|trans }} {{ context.duration|round(2) }}ms
{{ 'monitoring_queries'|trans({'%count%': 2}) }} {{ context.queries.count() }} in {{ queryDuration|round(2) }}ms ({{ (100 / context.duration * queryDuration)|round }}%)
{{ 'monitoring_twig_renders'|trans() }} {{ context.twigRenders.count() }} in {{ twigDuration|round(2) }}ms ({{ (100 / context.duration * twigDuration)|round }}%)
{{ 'monitoring_curl_requests'|trans() }} {{ context.curlRequests.count() }} in {{ curlRequestDuration|round(2) }}ms ({{ (100 / context.duration * curlRequestDuration)|round }}%)
{{ 'monitoring_duration_sending_response'|trans() }} {{ context.responseSendingDurationMilliseconds|round(2) }}ms ({{ (100 / context.duration * context.responseSendingDurationMilliseconds)|round }}%)
================================================
FILE: templates/admin/monitoring/_monitoring_single_queries.html.twig
================================================
{{ 'monitoring_queries'|trans({'%count%': 2}) }}
{% if groupSimilar %}
{{ 'monitoring_dont_group_similar'|trans }}
{% else %}
{{ 'monitoring_group_similar'|trans }}
{% endif %}
{% if formatQuery %}
{{ 'monitoring_dont_format_query'|trans }}
{% else %}
{{ 'monitoring_format_query'|trans }}
{% endif %}
{% if showParameters %}
{{ 'monitoring_dont_show_parameters'|trans }}
{% else %}
{{ 'monitoring_show_parameters'|trans }}
{% endif %}
{{ 'monitoring_queries'|trans({'%count%': 1}) }}
{% if groupSimilar is same as false %}
{{ 'monitoring_duration'|trans }}
{% else %}
{{ 'monitoring_duration_min'|trans }}
{{ 'monitoring_duration_mean'|trans }}
{{ 'monitoring_duration_max'|trans }}
{{ 'monitoring_query_count'|trans }}
{{ 'monitoring_query_total'|trans }}
{% endif %}
{% if groupSimilar is same as false %}
{% for query in context.getQueriesSorted() %}
{% if formatQuery %}
{{ query.queryString.query|formatQuery }}
{% else %}
{{ query.queryString.query }}
{% endif %}
{% if showParameters %}
{{ 'parameters'|trans }} = {{ query.parameters|json_encode(constant('JSON_PRETTY_PRINT')) }}
{% endif %}
{{ query.duration|round(2) }}ms
{% endfor %}
{% else %}
{% for query in context.getGroupedQueries() %}
{% if formatQuery %}
{{ query.query|formatQuery }}
{% else %}
{{ query.query }}
{% endif %}
{{ query.minExecutionTime|round(2) }}ms
{{ query.maxExecutionTime|round(2) }}ms
{{ query.meanExecutionTime|round(2) }}ms
{{ query.count }}
{{ query.totalExecutionTime|round(2) }}ms
{% endfor %}
{% endif %}
================================================
FILE: templates/admin/monitoring/_monitoring_single_requests.html.twig
================================================
{{ 'monitoring_http_method'|trans }}
{{ 'monitoring_url'|trans }}
{{ 'monitoring_request_successful'|trans }}
{{ 'monitoring_duration'|trans }}
{% for request in context.getRequestsSorted() %}
{{ request.method }}
{{ request.url }}
{{ request.wasSuccessful ? 'yes'|trans : 'no'|trans }}
{{ request.duration|round(2) }}ms
{% endfor %}
================================================
FILE: templates/admin/monitoring/_monitoring_single_twig.html.twig
================================================
{% if compareToParent %}
{{ 'monitoring_twig_compare_to_total'|trans }}
{% else %}
{{ 'monitoring_twig_compare_to_parent'|trans }}
{% endif %}
{% for render in context.getRootTwigRenders() %}
{{ component('monitoring_twig_render', {'render': render, 'compareToParent': compareToParent}) }}
{% endfor %}
================================================
FILE: templates/admin/monitoring/monitoring.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'pages'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-monitoring{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if chart is not same as null %}
{{ 'monitoring_route_overview'|trans }}
{% if configuration['monitoringEnabled'] is not same as true %}
{{ 'monitoring_disabled'|trans }}
{% else %}
{% if configuration['monitoringQueriesEnabled'] is same as true %}
{% if configuration['monitoringQueriesPersistingEnabled'] is same as true %}
{{ 'monitoring_queries_enabled_persisted'|trans }}
{% else %}
{{ 'monitoring_queries_enabled_not_persisted'|trans }}
{% endif %}
{% else %}
{{ 'monitoring_queries_disabled'|trans }}
{% endif %}
{% if configuration['monitoringTwigRendersEnabled'] is same as true %}
{% if configuration['monitoringTwigRendersPersistingEnabled'] is same as true %}
{{ 'monitoring_twig_renders_enabled_persisted'|trans }}
{% else %}
{{ 'monitoring_twig_renders_enabled_not_persisted'|trans }}
{% endif %}
{% else %}
{{ 'monitoring_twig_renders_disabled'|trans }}
{% endif %}
{% if configuration['monitoringCurlRequestsEnabled'] is same as true %}
{% if configuration['monitoringCurlRequestPersistingEnabled'] is same as true %}
{{ 'monitoring_curl_requests_enabled_persisted'|trans }}
{% else %}
{{ 'monitoring_curl_requests_enabled_not_persisted'|trans }}
{% endif %}
{% else %}
{{ 'monitoring_curl_requests_disabled'|trans }}
{% endif %}
{% endif %}
{{ 'monitoring_route_overview_description'|trans }}
{{ render_chart(chart, {'data-chart-unit-value': 'ms'}) }}
{% endif %}
{{ form_start(form) }}
{{ form_row(form.executionType) }}
{{ form_row(form.userType) }}
{{ form_row(form.path) }}
{{ form_row(form.handler) }}
{{ form_row(form.durationMinimum) }}
{{ form_row(form.hasException) }}
{{ form_row(form.createdFrom) }}
{{ form_row(form.createdTo) }}
{{ form_row(form.chartOrdering) }}
{{ form_row(form.submit, {'attr': {'class': 'btn btn__primary'}}) }}
{{ form_end(form) }}
{{ 'monitoring_user_type'|trans }}
{{ 'monitoring_path'|trans }}
{{ 'monitoring_handler'|trans }}
{{ 'monitoring_started'|trans }}
{{ 'monitoring_duration'|trans }}
{% for context in executionContexts %}
{{ context.uuid|uuidEnd }}
{% if context.executionType is same as 'messenger' %}
messenger
{% else %}
{{ context.userType }}
{% endif %}
{{ context.path }}
{{ context.handler }}
{{ context.startedAt|date }}
{{ context.duration|round(2) }}ms
{% endfor %}
{% if(executionContexts.haveToPaginate is defined and executionContexts.haveToPaginate) %}
{{ pagerfanta(executionContexts, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/monitoring/monitoring_single.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'pages'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-monitoring{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% include 'admin/monitoring/_monitoring_single_options.html.twig' %}
{% if page is same as 'overview' %}
{{ include('admin/monitoring/_monitoring_single_overview.html.twig') }}
{% elseif page is same as 'queries' %}
{{ include('admin/monitoring/_monitoring_single_queries.html.twig') }}
{% elseif page is same as 'twig' %}
{{ include('admin/monitoring/_monitoring_single_twig.html.twig') }}
{% elseif page is same as 'requests' %}
{{ include('admin/monitoring/_monitoring_single_requests.html.twig') }}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/pages.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'pages'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-settings page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{{ form_start(form) }}
{{ component('editor_toolbar', {id: 'page_body'}) }}
{{ form_row(form.body, {label: false, attr: {placeholder: 'body', 'data-controller': 'rich-textarea autogrow', 'data-entry-link-create-target': 'admin_pages'}}) }}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/admin/reports.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reports'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-federation{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{# global mods can see this page, but not navigate to any other menu option, so hiding it for now #}
{% if is_granted('ROLE_ADMIN') %}
{% include 'admin/_options.html.twig' %}
{% endif %}
{{ component('report_list', {reports: reports}) }}
{% endblock %}
================================================
FILE: templates/admin/settings.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'settings'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-settings page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{{ 'settings'|trans }}
{{ form_start(form) }}
{{ 'general'|trans }}
{{ form_row(form.KBIN_DOMAIN, {label: 'domain'}) }}
{{ form_row(form.KBIN_CONTACT_EMAIL, {label: 'contact_email'}) }}
{{ form_label(form.MBIN_DEFAULT_THEME, 'default_theme') }}
{{ form_widget(form.MBIN_DEFAULT_THEME, {attr: {'aria-label': 'change_theme'|trans}}) }}
{{ 'meta'|trans }}
{{ form_row(form.KBIN_META_TITLE, {label: 'title'}) }}
{{ form_row(form.KBIN_META_DESCRIPTION, {label: 'description'}) }}
{{ form_row(form.KBIN_META_KEYWORDS, {label: 'keywords'}) }}
{{ 'instance'|trans }}
{{ form_row(form.KBIN_TITLE, {label: 'title'}) }}
{{ form_label(form.MBIN_DOWNVOTES_MODE, 'downvotes_mode') }}
{{ form_widget(form.MBIN_DOWNVOTES_MODE, {attr: {'aria-label': 'change_downvotes_mode'|trans}}) }}
{{ form_label(form.KBIN_HEADER_LOGO, 'header_logo') }}
{{ form_widget(form.KBIN_HEADER_LOGO) }}
{{ form_label(form.KBIN_REGISTRATIONS_ENABLED, 'registrations_enabled') }}
{{ form_widget(form.KBIN_REGISTRATIONS_ENABLED) }}
{{ form_label(form.MBIN_SSO_REGISTRATIONS_ENABLED, 'sso_registrations_enabled') }}
{{ form_widget(form.MBIN_SSO_REGISTRATIONS_ENABLED) }}
{{ form_label(form.MBIN_SSO_ONLY_MODE, 'sso_only_mode') }}
{{ form_widget(form.MBIN_SSO_ONLY_MODE) }}
{{ form_label(form.KBIN_CAPTCHA_ENABLED, 'captcha_enabled') }}
{{ form_widget(form.KBIN_CAPTCHA_ENABLED) }}
{{ form_label(form.KBIN_MERCURE_ENABLED, 'mercure_enabled') }}
{{ form_widget(form.KBIN_MERCURE_ENABLED) }}
{{ form_label(form.KBIN_ADMIN_ONLY_OAUTH_CLIENTS, 'restrict_oauth_clients') }}
{{ form_widget(form.KBIN_ADMIN_ONLY_OAUTH_CLIENTS) }}
{{ form_label(form.MBIN_PRIVATE_INSTANCE, 'private_instance') }}
{{ form_widget(form.MBIN_PRIVATE_INSTANCE) }}
{{ form_label(form.KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN, 'federated_search_only_loggedin') }}
{{ form_widget(form.KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN) }}
{{ form_label(form.MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY, 'sidebar_sections_random_local_only') }}
{{ form_widget(form.MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY) }}
{% if form.MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY.vars.errors|length > 0 %}
{% for error in form.MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY.vars.errors %}
{{ error.message }}
{% endfor %}
{% endif %}
{{ form_label(form.MBIN_SIDEBAR_SECTIONS_USERS_LOCAL_ONLY, 'sidebar_sections_users_local_only') }}
{{ form_widget(form.MBIN_SIDEBAR_SECTIONS_USERS_LOCAL_ONLY) }}
{{ form_label(form.MBIN_RESTRICT_MAGAZINE_CREATION, 'restrict_magazine_creation') }}
{{ form_widget(form.MBIN_RESTRICT_MAGAZINE_CREATION) }}
{{ form_label(form.MBIN_SSO_SHOW_FIRST, 'sso_show_first') }}
{{ form_widget(form.MBIN_SSO_SHOW_FIRST) }}
{{ form_label(form.MBIN_NEW_USERS_NEED_APPROVAL, 'new_users_need_approval') }}
{{ form_widget(form.MBIN_NEW_USERS_NEED_APPROVAL) }}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/admin/signup_requests.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'signup_requests'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-federation{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{# global mods can see this page, but not navigate to any other menu option, so hiding it for now #}
{% if is_granted('ROLE_ADMIN') %}
{% include 'admin/_options.html.twig' %}
{% endif %}
{{ 'signup_requests_header'|trans }}
{{ 'signup_requests_paragraph'|trans }}
{% if username is defined and username is not same as null %}
{% endif %}
{% if requests|length %}
{% for request in requests %}
{{ component('user_inline', {user: request, showNewIcon: true}) }},
{{ component('date', {date: request.createdAt}) }}
{{ request.applicationText }}
{% endfor %}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/admin/users.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'users'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-admin-users{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'admin/_options.html.twig' %}
{% if(users.haveToPaginate is defined and users.haveToPaginate) %}
{{ pagerfanta(users, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if searchTerm is defined %}
{% endif %}
{% if withFederated is defined %}
"
{{ 'local'|trans }}
{{ 'federated'|trans }}
{% endif %}
{% if not users|length %}
{% else %}
{% endif %}
{% if(users.haveToPaginate is defined and users.haveToPaginate) %}
{{ pagerfanta(users, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/base.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set V_LEFT = constant('App\\Controller\\User\\ThemeSettingsController::LEFT') -%}
{%- set V_RIGHT = constant('App\\Controller\\User\\ThemeSettingsController::RIGHT') -%}
{%- set V_FIXED = constant('App\\Controller\\User\\ThemeSettingsController::FIXED') -%}
{%- set V_LAST_ACTIVE = constant('App\\Controller\\User\\ThemeSettingsController::LAST_ACTIVE') -%}
{%- set FONT_SIZE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_FONT_SIZE'), '100') -%}
{%- set THEME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_THEME'), mbin_default_theme()) -%}
{%- set PAGE_WIDTH = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_PAGE_WIDTH'), V_FIXED) -%}
{%- set ROUNDED_EDGES = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_ROUNDED_EDGES'), V_TRUE) -%}
{%- set TOPBAR = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_TOPBAR'), V_FALSE) -%}
{%- set FIXED_NAVBAR = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_FIXED_NAVBAR'), V_FALSE) -%}
{%- set SIDEBAR_POSITION = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_SIDEBAR_POSITION'), V_RIGHT) -%}
{%- set COMPACT = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_COMPACT'), V_FALSE) -%}
{%- set SUBSCRIPTIONS_SHOW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SHOW'), V_TRUE) -%}
{%- set SUBSCRIPTIONS_SEPARATE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR'), V_FALSE) -%}
{%- set SUBSCRIPTIONS_SAME_SIDE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SIDEBARS_SAME_SIDE'), V_FALSE) -%}
{%- set SUBSCRIPTIONS_SORT = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SORT'), V_LAST_ACTIVE) -%}
{%- block title -%}{{ kbin_meta_title() }}{%- endblock -%}
{% if magazine is defined and magazine and magazine.apIndexable is same as false %}
{% elseif user is defined and user and user.apIndexable is same as false %}
{% elseif entry is defined and entry and entry.user and entry.user.apIndexable is same as false %}
{% elseif post is defined and post and post.user and post.user.apIndexable is same as false %}
{% endif %}
{% if kbin_header_logo() %}
{% endif %}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
{% include 'layout/_header.html.twig' with {header_nav: block('header_nav')} %}
{{ component('announcement') }}
{% block body %}{% endblock %}
{% if not mbin_private_instance() or (mbin_private_instance() and app.user is defined and app.user is not same as null) %}
{% endif %}
{% if app.user is defined and app.user is not same as null and
SUBSCRIPTIONS_SHOW is not same as V_FALSE and
SUBSCRIPTIONS_SEPARATE is same as V_TRUE
%}
{{ component('sidebar_subscriptions', { openMagazine: magazine is defined ? magazine : null, user: app.user, sort: SUBSCRIPTIONS_SORT }) }}
{% endif %}
{% include 'layout/_topbar.html.twig' %}
0
================================================
FILE: templates/bookmark/_form_edit.html.twig
================================================
{{ form_start(form, {attr: {class: 'bookmark_edit'}}) }}
{{ form_row(form.name, {label: 'bookmark_list_create_label'}) }}
{{ form_row(form.isDefault, {label: 'bookmark_list_make_default', row_attr: {class: 'checkbox'}}) }}
{% set btn_label = is_create ? 'bookmark_list_create' : 'bookmark_list_edit' %}
{{ form_row(form.submit, {label: btn_label, attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/bookmark/_options.html.twig
================================================
{% set showFilterLabels = app.request.cookies.get('kbin_general_filter_labels')|default('on') %}
{{ criteria.getOption('sort')|trans }}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('time') != 'all') %}
{{ criteria.getOption('time')|trans }}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('type') != 'all') %}
{{ criteria.getOption('type')|trans }}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('federation') != 'all') %}
{{ criteria.federation|trans }}
{% endif %}
{% if lists is defined and lists is not empty %}
{% if showFilterLabels == 'on' or showFilterLabels == 'auto' %}
{{ list.name }}
{% endif %}
{% endif %}
================================================
FILE: templates/bookmark/edit.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'bookmarks_list'|trans({'%list%': list.name}) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-bookmarks{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'bookmarks_list_edit'|trans }}
{% include 'bookmark/_form_edit.html.twig' with {is_create: false} %}
{% endblock %}
================================================
FILE: templates/bookmark/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'bookmarks_list'|trans({'%list%': list.name}) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-bookmarks{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'bookmarks'|trans }}
{% include 'bookmark/_options.html.twig' %}
{% endblock %}
================================================
FILE: templates/bookmark/overview.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'bookmark_lists'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-bookmark-lists{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'bookmark_lists'|trans }}
{% include('bookmark/_form_edit.html.twig') with {is_create: true} %}
{% if lists|length %}
{{ 'name'|trans }}
{{ 'count'|trans }}
{% for list in lists %}
{% if list.isDefault %}
{% endif %}
{{ list.name }}
{{ get_bookmark_list_entry_count(list) }}
{% if not list.isDefault %}
{% endif %}
{% endfor %}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig
================================================
{% extends '@!NelmioApiDoc/SwaggerUi/index.html.twig' %}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block header_block %}
{% endblock %}
================================================
FILE: templates/bundles/TwigBundle/Exception/error.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'login'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-error{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% endblock %}
================================================
FILE: templates/bundles/TwigBundle/Exception/error403.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'error'|trans }} 403 - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-error{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{'errors.server403.title'|trans}}
{% endblock %}
================================================
FILE: templates/bundles/TwigBundle/Exception/error404.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'error'|trans }} 404 - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-error{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{'errors.server404.title'|trans}}
{% endblock %}
================================================
FILE: templates/bundles/TwigBundle/Exception/error429.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'login'|trans }} 429 - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-error{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{'errors.server429.title'|trans}}
{% endblock %}
================================================
FILE: templates/bundles/TwigBundle/Exception/error500.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'login'|trans }} 500 - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-error{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{'errors.server500.title'|trans}}
{{ 'errors.server500.description'|trans({
'%link_start%': '',
'%link_end%': ' '
})|raw }}
{% endblock %}
================================================
FILE: templates/components/_ajax.html.twig
================================================
{{ component(component, attributes) }}
================================================
FILE: templates/components/_cached.html.twig
================================================
{{ this.getHtml(attributes)|raw }}
================================================
FILE: templates/components/_comment_collapse_button.html.twig
================================================
================================================
FILE: templates/components/_details_label.css.twig
================================================
:root {
--mbin-details-detail-label: "{{ 'details'|trans|e('css') }}";
--mbin-details-spoiler-label: "{{ 'spoiler'|trans|e('css') }}";
}
================================================
FILE: templates/components/_entry_comments_nested_hidden_private_threads.html.twig
================================================
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::ENTRY_COMMENTS_VIEW')) is same as constant('App\\Controller\\User\\ThemeSettingsController::CLASSIC') %}
{% for reply in comment.nested %}
{% if reply.visibility is same as 'private' %}
{% if app.user and reply.user.isFollower(app.user) %}
{% if not app.user.isBlocked(reply.user) %}
{{ component('entry_comment', {comment: reply, showNested: false, level: 3, showEntryTitle:false, showMagazineName:false}) }}
{% endif %}
{% else %}
{{ 'Private' }}
{% endif %}
{% else %}
{% if not app.user or (app.user and not app.user.isBlocked(reply.user)) %}
{{ component('entry_comment', {comment: reply, showNested: false, level: 3, showEntryTitle:false, showMagazineName:false}) }}
{% endif %}
{% endif %}
{% endfor %}
{% else %}
{% for reply in comment.children %}
{% if reply.visibility is same as 'private' %}
{% if app.user and reply.user.isFollower(app.user) %}
{% if not app.user.isBlocked(reply.user) %}
{{ component('entry_comment', {comment: reply, showNested:true, level: level + 1, showEntryTitle:false, showMagazineName:false}) }}
{% endif %}
{% else %}
{{ 'Private' }}
{% endif %}
{% else %}
{% if not app.user or (app.user and not app.user.isBlocked(reply.user)) %}
{{ component('entry_comment', {comment: reply, showNested:true, level: level + 1, showEntryTitle:false, showMagazineName:false}) }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/_figure_entry.html.twig
================================================
{# this fragment is only meant to be used in entry component #}
{% with {is_single: is_route_name('entry_single'), image: entry.image} %}
{% set sensitive_id = 'sensitive-check-%s-%s'|format(entry.id, image.id) %}
{% set lightbox_alt_id = 'thumb-alt-%s-%s'|format(entry.id, image.id) %}
{% set image_path = image.filePath ? asset(image.filePath)|imagine_filter('entry_thumb') : image.sourceUrl %}
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set LIST_LIGHTBOX = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_LIST_IMAGE_LIGHTBOX'), V_TRUE) -%}
{% if LIST_LIGHTBOX is same as V_TRUE %}
{% set route = uploaded_asset(image) %}
{% elseif type is same as 'image' %}
{% set route = is_single ? uploaded_asset(image) : entry_url(entry) %}
{% elseif type is same as 'link' %}
{% set route = is_single ? entry.url : entry_url(entry) %}
{% endif %}
{% set is_single_image = is_single and type is same as 'image' %}
{% if image.altText %}
{{ image.altText|nl2br }}
{% endif %}
{% if image.blurhash %}
{{ component('blurhash_image', {blurhash: image.blurhash}) }}
{% endif %}
{% if entry.isAdult %}
{% endif %}
{% if entry.isAdult %}
{% endif %}
{% endwith %}
================================================
FILE: templates/components/_figure_image.html.twig
================================================
{% with {
sensitive_id: 'sensitive-check-%s-%s'|format(parent_id, image.id),
lightbox_alt_id: 'thumb-alt-%s-%s'|format(parent_id, image.id),
image_path: (image.filePath ? asset(image.filePath)|imagine_filter(thumb_filter) : image.sourceUrl)
} %}
{% if image.altText %}
{{ image.altText|nl2br }}
{% endif %}
{% endwith %}
================================================
FILE: templates/components/_loading_icon.html.twig
================================================
================================================
FILE: templates/components/_post_comments_nested_hidden_private_threads.html.twig
================================================
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::ENTRY_COMMENTS_VIEW')) is same as constant('App\\Controller\\User\\ThemeSettingsController::CLASSIC') %}
{% for reply in comment.nested %}
{% if reply.visibility is same as 'private' %}
{% if app.user and reply.user.isFollower(app.user) %}
{% if not app.user.isBlocked(reply.user) %}
{{ component('post_comment', {comment: reply, showNested:false, level: 3}) }}
{% endif %}
{% else %}
{{ 'Private' }}
{% endif %}
{% else %}
{% if not app.user or (app.user and not app.user.isBlocked(reply.user)) %}
{{ component('post_comment', {comment: reply, showNested:false, level: 3}) }}
{% endif %}
{% endif %}
{% endfor %}
{% else %}
{% for reply in comment.children %}
{% if reply.visibility is same as 'private' %}
{% if app.user and reply.user.isFollower(app.user) %}
{% if not app.user.isBlocked(reply.user) %}
{{ component('entry_comment', {comment: reply, showNested:true, level: level + 1, showEntryTitle:false, showMagazineName:false}) }}
{% endif %}
{% else %}
{{ 'Private' }}
{% endif %}
{% else %}
{% if not app.user or (app.user and not app.user.isBlocked(reply.user)) %}
{{ component('post_comment', {comment: reply, showNested:true, level: level + 1}) }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/_settings_row_enum.html.twig
================================================
================================================
FILE: templates/components/_settings_row_switch.html.twig
================================================
================================================
FILE: templates/components/active_users.html.twig
================================================
{% if users|length %}
{{ 'active_users'|trans }}
{% for user in users %}
{{ component('user_avatar', {user: user, width: 65, height: 65, asLink: true}) }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/announcement.html.twig
================================================
{% if content is not empty %}
{{ content|markdown|raw }}
{% endif %}
================================================
FILE: templates/components/blurhash_image.html.twig
================================================
================================================
FILE: templates/components/bookmark_list.html.twig
================================================
{% if is_bookmarked_in_list(app.user, list, subject) %}
{{ 'bookmark_remove_from_list'|trans({'%list%': list.name}) }}
{% else %}
{{ 'bookmark_add_to_list'|trans({'%list%': list.name}) }}
{% endif %}
================================================
FILE: templates/components/bookmark_menu_list.html.twig
================================================
================================================
FILE: templates/components/bookmark_standard.html.twig
================================================
{% if is_bookmarked(app.user, subject) %}
{% else %}
{% endif %}
================================================
FILE: templates/components/boost.html.twig
================================================
{%- set VOTE_UP = constant('App\\Entity\\Contracts\\VotableInterface::VOTE_UP') -%}
{%- set user_choice = is_granted('ROLE_USER') ? subject.userChoice(app.user) : null -%}
================================================
FILE: templates/components/cursor_pagination.html.twig
================================================
================================================
FILE: templates/components/date.html.twig
================================================
{{ date|ago }}
================================================
FILE: templates/components/date_edited.html.twig
================================================
{% if editedAt %}
{% if editedAt|date('U') - createdAt|date('U') > 300 %}
({{ 'edited'|trans }}
{{ editedAt|ago }} )
{% endif %}
{% endif %}
================================================
FILE: templates/components/domain.html.twig
================================================
{{ 'domain'|trans }}
{{ component('domain_sub', {domain: domain}) }}
================================================
FILE: templates/components/domain_sub.html.twig
================================================
================================================
FILE: templates/components/editor_toolbar.html.twig
================================================
================================================
FILE: templates/components/entries_cross.html.twig
================================================
{% if entries|length %}
{% set batch_size = entries|length % 2 == 0 ? 2 : 3 %}
{% for entry_batch in entries|batch(batch_size) %}
{% for entry in entry_batch %}
{{ component('entry_cross', {entry: entry}) }}
{% endfor %}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/entry.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_PREVIEW'), V_FALSE) -%}
{%- set SHOW_THUMBNAILS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_THUMBNAILS'), V_TRUE) -%}
{%- set SHOW_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_USERS_AVATARS'), V_TRUE) -%}
{%- set SHOW_MAGAZINE_ICONS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_MAGAZINES_ICONS'), V_TRUE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if not app.user or (app.user and not app.user.isBlocked(entry.user)) %}
{% if entry.visibility is same as 'private' and (not app.user or not app.user.isFollower(entry.user)) %}
Private
{% elseif entry.cross %}
{{ component('entry_cross', {entry: entry}) }}
{% else %}
{% if entry.visibility in ['visible', 'private'] or (entry.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% if entry.body and showShortSentence %}
{{ get_short_sentence(entry.body|markdown|raw, striptags = true) }}
{% endif %}
{% if entry.body and showBody %}
{{ entry.body|markdown("entry")|raw }}
{% endif %}
{% endif %}
{{ component('user_inline', {user: entry.user, showAvatar: SHOW_USER_AVATARS is same as V_TRUE, showNewIcon: true, fullName: SHOW_USER_FULLNAME is same as V_TRUE}) -}}
{% if (entry.user.type) == "Service" %}
{{ 'user_badge_bot'|trans }}
{% endif %}
{% if entry.user.admin() %}
{{ 'user_badge_admin'|trans }}
{% elseif entry.user.moderator() %}
{{ 'user_badge_global_moderator'|trans }}
{% elseif entry.magazine.userIsModerator(entry.user) %}
{{ 'user_badge_moderator'|trans }}
{% endif %}
,
{{ component('date', {date: entry.createdAt}) }}
{{ component('date_edited', {createdAt: entry.createdAt, editedAt: entry.editedAt}) }}
{% if showMagazineName %}
{{ 'to'|trans }} {{ component('magazine_inline', {magazine: entry.magazine, showAvatar: SHOW_MAGAZINE_ICONS is same as V_TRUE, showNewIcon: true, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }}
{% endif %}
{% if SHOW_THUMBNAILS is same as V_TRUE %}
{% if entry.image %}
{% if entry.type is same as 'link' or entry.type is same as 'video' %}
{{ include('components/_figure_entry.html.twig', {entry: entry, type: 'link'}) }}
{% elseif entry.type is same as 'image' or entry.type is same as 'article' %}
{{ include('components/_figure_entry.html.twig', {entry: entry, type: 'image'}) }}
{% endif %}
{% else %}
{% endif %}
{% endif %}
{% if entry.visibility in ['visible', 'private'] %}
{{ component('vote', {
subject: entry,
}) }}
{% endif %}
{% if entry.visibility in ['visible', 'private'] %}
{% if entry.sticky %}
{% endif %}
{% if entry.type is same as 'article' %}
{% elseif entry.type is same as 'link' and entry.hasEmbed is same as false %}
{% endif %}
{% if entry.hasEmbed %}
{% set preview_url = entry.type is same as 'image' and entry.image ? uploaded_asset(entry.image) : entry.url %}
{% endif %}
{% if not entry.isLocked %}
{{ entry.commentCount|abbreviateNumber }} {{ 'comments_count'|trans({'%count%': entry.commentCount}) }}
{% else %}
{{ entry.commentCount|abbreviateNumber }} {{ 'comments_count'|trans({'%count%': entry.commentCount}) }}
{% endif %}
{{ component('boost', {
subject: entry
}) }}
{% if app.user is defined and app.user is not same as null %}
{{ component('bookmark_standard', { subject: entry }) }}
{% endif %}
{% include 'entry/_menu.html.twig' %}
{% if app.user is defined and app.user is not same as null and not showShortSentence %}
{{ component('notification_switch', {target: entry}) }}
{% endif %}
Loading...
{% elseif (entry.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, entry) %}
{{ component('bookmark_standard', { subject: entry }) }}
{% endif %}
Loading...
{% else %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, entry) %}
{{ component('bookmark_standard', { subject: entry }) }}
{% endif %}
Loading...
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: templates/components/entry_comment.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set V_TREE = constant('App\\Controller\\User\\ThemeSettingsController::TREE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_PREVIEW'), V_FALSE) -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set VIEW_STYLE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::ENTRY_COMMENTS_VIEW'), V_TREE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if not app.user or (app.user and not app.user.isBlocked(comment.user)) %}
{% if comment.visibility is same as 'private' and (not app.user or not app.user.isFollower(comment.user)) %}
{% else %}
{% endif %}
{% if showNested %}
{{ component('entry_comments_nested', {
comment: comment,
level: level,
showNested: true,
view: VIEW_STYLE,
criteria: criteria,
}) }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/entry_comment_inline_md.html.twig
================================================
{% if rich is same as true %}
{% else %}
{% if comment.apId is same as null %}
{{ url('entry_comment_view', {magazine_name: comment.magazine.name, entry_id: comment.entry.id, comment_id: comment.id, slug: '-'}) }}
{% else %}
{{ comment.apId }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/entry_comments_nested.html.twig
================================================
{% if view is same as constant('App\\Controller\\User\\ThemeSettingsController::CLASSIC') %}
{% for reply in comment.nested %}
{{ component('entry_comment', {
comment: reply,
showNested: false,
level: 3,
showEntryTitle: false,
showMagazineName: false
}) }}
{% endfor %}
{% else %}
{% for reply in comment.getChildrenByCriteria(criteria, mbin_downvotes_mode(), app.user, 'comments') %}
{{ component('entry_comment', {
comment: reply,
showNested: true,
level: level + 1,
showEntryTitle: false,
showMagazineName: false,
criteria: criteria,
}) }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/entry_cross.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if entry.visibility in ['visible', 'private'] or (entry.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% elseif(entry.visibility is same as 'trashed') %}
[{{ 'deleted_by_moderator'|trans }} ]
{% elseif(entry.visibility is same as 'soft_deleted') %}
[{{ 'deleted_by_author'|trans }} ]
{% endif %}
{{ component('user_inline', {user: entry.user, showAvatar: false, showNewIcon: true, fullName: SHOW_USER_FULLNAME is same as V_TRUE}) -}}
,
{{ component('date', {date: entry.createdAt}) }}
{{ component('date_edited', {createdAt: entry.createdAt, editedAt: entry.editedAt}) }}
{{ 'to'|trans }} {{ component('magazine_inline', {magazine: entry.magazine, showAvatar: false, showNewIcon: true, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }}
{% if entry.visibility in ['visible', 'private'] %}
{{ component('vote', {
subject: entry,
}) }}
{% endif %}
================================================
FILE: templates/components/entry_inline.html.twig
================================================
{{ entry.title }}
================================================
FILE: templates/components/entry_inline_md.html.twig
================================================
{% if rich is same as true %}
{{ component('user_inline', {user: entry.user, fullName: userFullName, showNewIcon: true}) }}:
{% if entry.image is not same as null %}
{% endif %}
{{ entry.title }}
{% if entry.magazine.name is not same as 'random' %}
{{ 'in'|trans }}
{{ component('magazine_inline', {magazine: entry.magazine, fullName: magazineFullName, showNewIcon: true}) }}
{% endif %}
{% else %}
{% if entry.apId is same as null %}
{{ url('entry_single', {magazine_name: entry.magazine.name, entry_id: entry.id, slug: '-'}, ) }}
{% else %}
{{ entry.apId }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/favourite.html.twig
================================================
================================================
FILE: templates/components/featured_magazines.html.twig
================================================
{% for mag in magazines %}
{{ mag }}
{% endfor %}
================================================
FILE: templates/components/filter_list.html.twig
================================================
{{ 'filter_lists_filter_words'|trans }}:
{{ list.words|map(x => x.word)|join(', ') }}
{{ 'filter_lists_filter_location'|trans }}:
{{ list.getRealmStrings()|map(location => location|trans)|join(', ') }}
================================================
FILE: templates/components/instance_list.html.twig
================================================
{{ 'domain'|trans }}
{{ 'server_software'|trans }}
{{ 'version'|trans }}
{% if app.user is defined and app.user is not same as null and app.user.admin %}
{{ 'last_successful_deliver'|trans }}
{{ 'last_successful_receive'|trans }}
{% if showUnBanButton or showBanButton or showDenyButton or showAllowButton %}
{% endif %}
{% endif %}
{% for instance in instances %}
{{instance.domain}}
{{ instance.software ?? '' }}
{{ instance.version ?? '' }}
{% if app.user is defined and app.user is not same as null and app.user.admin %}
{% if instance.lastSuccessfulDeliver is not same as null %}
{{ component('date', { date: instance.lastSuccessfulDeliver }) }}
{% endif %}
{% if instance.lastSuccessfulReceive is not same as null %}
{{ component('date', { date: instance.lastSuccessfulReceive }) }}
{% endif %}
{% if showUnBanButton or showBanButton or showDenyButton or showAllowButton %}
{% if showUnBanButton and instance.isBanned %}
{% endif %}
{% if showBanButton and not instance.isBanned %}
{% endif %}
{% if showDenyButton and instance.isExplicitlyAllowed %}
{% endif %}
{% if showAllowButton and not instance.isExplicitlyAllowed %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
================================================
FILE: templates/components/loader.html.twig
================================================
================================================
FILE: templates/components/login_socials.html.twig
================================================
{# @var this App\Twig\Components\LoginSocialsComponent #}
{%- set HAS_ANY_SOCIAL = this.googleEnabled or this.facebookEnabled or this.discordEnabled or this.githubEnabled or this.keycloakEnabled or this.simpleloginEnabled or this.zitadelEnabled or this.authentikEnabled or this.azureEnabled or this.privacyPortalEnabled -%}
{% if HAS_ANY_SOCIAL %}
{% if not mbin_sso_only_mode() and not mbin_sso_show_first() %}
{% endif %}
{% if not mbin_sso_only_mode() and mbin_sso_show_first() %}
{% endif %}
{% endif %}
================================================
FILE: templates/components/magazine_box.html.twig
================================================
{% if showSectionTitle %}
{{ 'magazine'|trans }}
{% endif %}
{% if app.user and (magazine.userIsOwner(app.user) or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR')) and not is_route_name_contains('magazine_panel') %}
{% endif %}
{% if computed.magazine.icon and showCover and (app.user or magazine.isAdult is same as false) %}
{% endif %}
{{ component('magazine_sub', {magazine: magazine}) }}
{% if app.user is defined and app.user is not same as null %}
{{ component('notification_switch', {target: magazine}) }}
{% endif %}
{% if computed.magazine.description and showDescription %}
{{ computed.magazine.description|markdown|raw }}
{% endif %}
{% if computed.magazine.rules and showRules %}
{{ 'rules'|trans }}
{{ computed.magazine.rules|markdown|raw }}
{% endif %}
{% if showInfo %}
{% endif %}
{% if showMeta %}
{% endif %}
{% macro meta_item(name, url, count) %}
{{ name }} {{ count }}
{% endmacro %}
{% if showTags and magazine.tags %}
{{ 'tags'|trans }}
{% for tag in magazine.tags %}
#{{ tag }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/magazine_inline.html.twig
================================================
{% if magazine.icon and showAvatar and (app.user or magazine.isAdult is same as false) %}
{% endif %}
{{ magazine.title -}}
{%- if fullName -%}
@{{- magazine.name|apDomain -}}
{%- endif -%}
{% if magazine.isAdult %} 18+ {% endif %}
{% if magazine.postingRestrictedToMods %}
{% endif %}
{% if magazine.isNew() and showNewIcon %}
{% set days = constant('App\\Entity\\Magazine::NEW_FOR_DAYS') %}
{% endif %}
================================================
FILE: templates/components/magazine_inline_md.html.twig
================================================
{% if rich is same as true %}
{{ component('magazine_inline', {
magazine: magazine,
stretchedLink: stretchedLink,
fullName: fullName,
showAvatar: showAvatar,
}) }}
{% else %}
!{{- magazine.name|username -}}
{%- if fullName -%}
@{{- magazine.name|apDomain -}}
{%- endif -%}
{% endif %}
================================================
FILE: templates/components/magazine_sub.html.twig
================================================
================================================
FILE: templates/components/monitoring_twig_render.html.twig
================================================
{% if compareToParent %}
{{ render.shortDescription }} | {{ render.profilerDuration|round(2) }}ms / {{ render.getPercentageOfParentDuration()|round }}%
{% else %}
{{ render.shortDescription }} | {{ render.profilerDuration|round(2) }}ms / {{ render.getPercentageOfTotalDuration()|round }}%
{% endif %}
{% for child in render.children %}
{{ component('monitoring_twig_render', {'render': child, 'compareToParent': compareToParent}) }}
{% endfor %}
================================================
FILE: templates/components/notification_switch.html.twig
================================================
================================================
FILE: templates/components/post.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_PREVIEW'), V_FALSE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if not app.user or (app.user and not app.user.isBlocked(post.user)) %}
{% if post.visibility is same as 'private' and (not app.user or not app.user.isFollower(post.user)) %}
Private
{% else %}
{% if post.visibility in ['visible', 'private'] %}
{{ component('vote', {
subject: post,
showDownvote: false
}) }}
{% endif %}
{% if post.isAdult %}18+ {% endif %}
{{ component('user_inline', {user: post.user, showAvatar: true, showNewIcon: true, fullName: SHOW_USER_FULLNAME is same as V_TRUE}) }}
{% if (post.user.type) == "Service" %}
{{ 'user_badge_bot'|trans }}
{% endif %}
{% if post.user.admin() %}
{{ 'user_badge_admin'|trans }}
{% elseif post.user.moderator() %}
{{ 'user_badge_global_moderator'|trans }}
{% elseif post.magazine.userIsModerator(post.user) %}
{{ 'user_badge_moderator'|trans }}
{% endif %}
,
{% if dateAsUrl %}
{{ component('date', {date: post.createdAt}) }}
{% else %}
{{ component('date', {date: post.createdAt}) }}
{% endif %}
{{ component('date_edited', {createdAt: post.createdAt, editedAt: post.editedAt}) }}
{% if showMagazineName %}{{ 'to'|trans }} {{ component('magazine_inline', {magazine: post.magazine, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE, showNewIcon: true}) }}{% endif %}
{% if post.lang is not same as app.request.locale and post.lang is not same as kbin_default_lang() %}
{{ post.lang|language_name }}
{% endif %}
{% if post.visibility in ['visible', 'private'] or (post.visibility is same as 'trashed' and this.canSeeTrashed) %}
{{ post.body|markdown("post")|raw }}
{% elseif(post.visibility is same as 'trashed') %}
[{{ 'deleted_by_moderator'|trans }} ]
{% elseif(post.visibility is same as 'soft_deleted') %}
[{{ 'deleted_by_author'|trans }} ]
{% endif %}
{% if post.image %}
{{ include('components/_figure_image.html.twig', {
image: post.image,
parent_id: post.id,
is_adult: post.isAdult,
thumb_filter: 'post_thumb',
gallery_name: 'post-%d'|format(post.id),
}) }}
{% endif %}
{% if post.visibility in ['visible', 'private'] %}
{% if post.sticky %}
{% endif %}
{% if not post.isLocked %}
{{ 'reply'|trans }}
{% else %}
{{ 'reply'|trans }}
{% endif %}
{% if not is_route_name('post_single', true) and ((not showCommentsPreview and post.commentCount > 0) or post.commentCount > 2) %}
{{ 'expand'|trans }} ({{ post.commentCount|abbreviateNumber }} )
{{ 'collapse'|trans }}
({{ post.commentCount|abbreviateNumber }})
{% endif %}
{{ component('boost', {
subject: post
}) }}
{% if app.user is defined and app.user is not same as null %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
{% include 'post/_menu.html.twig' %}
{% if app.user is defined and app.user is not same as null and isSingle is defined and isSingle %}
{{ component('notification_switch', {target: post}) }}
{% endif %}
Loading...
{{ component('voters_inline', {
subject: post,
url: post_voters_url(post, 'up'),
'data-post-target': 'voters'
}) }}
{% elseif(post.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, post) %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
Loading...
{% else %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, post) %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
Loading...
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: templates/components/post_combined.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_PREVIEW'), V_FALSE) -%}
{%- set SHOW_THUMBNAILS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_THUMBNAILS'), V_TRUE) -%}
{%- set SHOW_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_USERS_AVATARS'), V_TRUE) -%}
{%- set SHOW_MAGAZINE_ICONS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_MAGAZINES_ICONS'), V_TRUE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if not app.user or (app.user and not app.user.isBlocked(post.user)) %}
{% if post.visibility is same as 'private' and (not app.user or not app.user.isFollower(post.user)) %}
Private
{% else %}
{% with %}
{% set hasTitle = post.visibility in ['visible', 'private'] or (post.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% set isAdult = post.isAdult %}
{% set hasLang = post.lang is not same as app.request.locale and post.lang is not same as kbin_default_lang() %}
{% set isModDeleted = post.visibility is same as 'trashed' %}
{% set isUserDeleted = post.visibility is same as 'soft_deleted' %}
{% set needsHeader = (hasTitle and (isAdult or hasLang)) or isModDeleted or isUserDeleted %}
{% endwith %}
{% if post.visibility in ['visible', 'private'] or (post.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% if post.body %}
{{ get_short_sentence(post.body|markdown|raw, striptags = true, onlyFirstParagraph = false) }}
{% endif %}
{% endif %}
{{ component('user_inline', {user: post.user, showAvatar: SHOW_USER_AVATARS is same as V_TRUE, showNewIcon: true, fullName: SHOW_USER_FULLNAME is same as V_TRUE}) -}}
{% if (post.user.type) == "Service" %}
{{ 'user_badge_bot'|trans }}
{% endif %}
{% if post.user.admin() %}
{{ 'user_badge_admin'|trans }}
{% elseif post.user.moderator() %}
{{ 'user_badge_global_moderator'|trans }}
{% elseif post.magazine.userIsModerator(post.user) %}
{{ 'user_badge_moderator'|trans }}
{% endif %}
,
{{ component('date', {date: post.createdAt}) }}
{{ component('date_edited', {createdAt: post.createdAt, editedAt: post.editedAt}) }}
{% if showMagazineName %}
{{ 'to'|trans }} {{ component('magazine_inline', {magazine: post.magazine, showAvatar: SHOW_MAGAZINE_ICONS is same as V_TRUE, showNewIcon: true, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }}
{% endif %}
{% if SHOW_THUMBNAILS is same as V_TRUE %}
{% if post.image %}
{{ include('components/_figure_entry.html.twig', {entry: post, type: 'image'}) }}
{% else %}
{% endif %}
{% endif %}
{% if post.visibility in ['visible', 'private'] %}
{{ component('vote', {
subject: post,
showDownvote: false
}) }}
{% endif %}
{% if post.visibility in ['visible', 'private'] %}
{% if post.sticky %}
{% endif %}
{% if post.image %}
{% endif %}
{{ post.commentCount }} {{ 'comments_count'|trans({'%count%': post.commentCount}) }}
{{ component('boost', {
subject: post
}) }}
{% if app.user is defined and app.user is not same as null %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
{% include 'post/_menu.html.twig' %}
Loading...
{% elseif (post.visibility is same as 'trashed' and this.canSeeTrashed) %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, post) %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
Loading...
{% else %}
{% if app.user is defined and app.user is not same as null and is_bookmarked(app.user, post) %}
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
Loading...
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: templates/components/post_comment.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set V_TREE = constant('App\\Controller\\User\\ThemeSettingsController::TREE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_PREVIEW'), V_FALSE) -%}
{%- set VIEW_STYLE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::POST_COMMENTS_VIEW'), V_TREE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{% if withPost %}
{{ component('post', {post: comment.post}) }}
{% endif %}
{% if not app.user or (app.user and not app.user.isBlocked(comment.user)) %}
{% if comment.visibility is same as 'private' and (not app.user or not app.user.isFollower(comment.user)) %}
{% else %}
{% endif %}
{% if showNested %}
{{ component('post_comments_nested', {
comment: comment,
level: level,
showNested: true,
view: VIEW_STYLE,
criteria: criteria,
}) }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/post_comment_combined.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_PREVIEW = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_PREVIEW'), V_FALSE) -%}
{%- set SHOW_THUMBNAILS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_THUMBNAILS'), V_TRUE) -%}
{%- set SHOW_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_USERS_AVATARS'), V_TRUE) -%}
{%- set SHOW_MAGAZINE_ICONS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_ENTRIES_SHOW_MAGAZINES_ICONS'), V_TRUE) -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if not app.user or (app.user and not app.user.isBlocked(comment.user)) %}
{% if comment.visibility is same as 'private' and (not app.user or not app.user.isFollower(comment.user)) %}
Private
{% else %}
{% endif %}
{% endif %}
================================================
FILE: templates/components/post_comment_inline_md.html.twig
================================================
{% if rich is same as true %}
{% else %}
{% if comment.apId is same as null %}
{{ url('post_single', {magazine_name: comment.magazine.name, post_id: comment.post.id, slug: '-'}) }}#post-comment-{{ comment.id }}
{% else %}
{{ comment.apId }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/post_comments_nested.html.twig
================================================
{% if view is same as constant('App\\Controller\\User\\ThemeSettingsController::CLASSIC') %}
{% for reply in comment.nested %}
{{ component('post_comment', {
comment: reply,
showNested: false,
level: 3,
criteria: criteria,
}) }}
{% endfor %}
{% else %}
{% for reply in comment.getChildrenByCriteria(criteria, app.user, 'comments') %}
{{ component('post_comment', {
comment: reply,
showNested: true,
level: level + 1,
criteria: criteria,
}) }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/post_comments_preview.html.twig
================================================
{% for comment in comments %}
{{ component('post_comment', {
comment: comment,
showNested: false,
level: 2,
}) }}
{% endfor %}
================================================
FILE: templates/components/post_inline_md.html.twig
================================================
{% if rich is same as true %}
{{ component('user_inline', {user: post.user, fullName: userFullName, showNewIcon: true}) }}:
{% if post.image is not same as null %}
{% endif %}
{{ post.getShortTitle() }}
{% if post.magazine.name is not same as 'random' %}
{{ 'in'|trans }}
{{ component('magazine_inline', {magazine: post.magazine, fullName: magazineFullName}) }}
{% endif %}
{% else %}
{% if post.apId is same as null %}
{{ url('post_single', {magazine_name: post.magazine.name, post_id: post.id, slug: '-'}) }}
{% else %}
{{ post.apId }}
{% endif %}
{% endif %}
================================================
FILE: templates/components/related_entries.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if entries|length %}
{{ title|trans }}
{% for entry in entries %}
{{ component('date', {date: entry.createdAt}) }} {{ 'to'|trans }} {{ component('magazine_inline', {magazine: entry.magazine, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/related_magazines.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if magazines|length %}
{% endif %}
================================================
FILE: templates/components/related_posts.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if posts|length %}
{{ title|trans }}
{% for post in posts %}
{% if post.image %}
{% endif %}
{% if post.body %}
{{ get_short_sentence(post.body)|markdown|raw }}
{% endif %}
{{ 'show_more'|trans }}
{{ component('date', {date: post.createdAt}) }} {{ 'to'|trans }} {{ component('magazine_inline', {magazine: post.magazine, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE, showNewIcon: true}) }}
{% endfor %}
{% endif %}
================================================
FILE: templates/components/report_list.html.twig
================================================
{%- set REPORT_ANY = constant('App\\Entity\\Report::STATUS_ANY') -%}
{%- set REPORT_PENDING = constant('App\\Entity\\Report::STATUS_PENDING') -%}
{%- set REPORT_APPROVED = constant('App\\Entity\\Report::STATUS_APPROVED') -%}
{%- set REPORT_REJECTED = constant('App\\Entity\\Report::STATUS_REJECTED') -%}
{%- set REPORT_CLOSED = constant('App\\Entity\\Report::STATUS_CLOSED') -%}
{% for report in reports %}
{{ component('user_inline', {user: report.reporting, showNewIcon: true}) }},
{{ component('date', {date: report.createdAt}) }}
{% include 'layout/_subject_link.html.twig' with {subject: report.subject} -%}
{{ report.reason }}
{% if app.request.get('status') is same as REPORT_ANY %}
{{ report.status }}
{% endif %}
{% if report.status is not same as REPORT_CLOSED %}
{% if report.status is not same as REPORT_REJECTED %}
{% endif %}
{% if report.status is not same as REPORT_APPROVED %}
{% endif %}
{% endif %}
{{ 'ban'|trans }} ({{ report.reported.username|username(true) }})
{% endfor %}
{% if(reports.haveToPaginate is defined and reports.haveToPaginate) %}
{{ pagerfanta(reports, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not reports|length %}
{% endif %}
================================================
FILE: templates/components/tag_actions.html.twig
================================================
================================================
FILE: templates/components/user_actions.html.twig
================================================
{{ user.followersCount|abbreviateNumber }}
{% if not app.user or app.user is not same as user and not is_instance_of_user_banned(user) %}
{% elseif app.user is same as user and is_route_name_starts_with('user') and not is_route_name_contains('settings') %}
{{ 'edit_my_profile'|trans }}
{% endif %}
{% if user.markedForDeletionAt and is_granted("ROLE_ADMIN") %}
{{ 'marked_for_deletion'|trans }}
{{ user.markedForDeletionAt|date }}
{% endif %}
================================================
FILE: templates/components/user_avatar.html.twig
================================================
{% if asLink %}
{% endif %}
{% if user.avatar %}
{% else %}
{% endif %}
{% if asLink %}
{% endif %}
================================================
FILE: templates/components/user_box.html.twig
================================================
{% if user.cover %}
{% if app.user is same as user and is_route_name_starts_with('user') and not is_route_name_contains('settings') %}
{% endif %}
{% if app.user is same as user and is_route_name_starts_with('user') and not is_route_name_contains('settings') %}
{% endif %}
{% endif %}
{% if user.avatar %}
{% if app.user is same as user and is_route_name_starts_with('user') and not is_route_name_contains('settings') %}
{% endif %}
{{ component('user_avatar', {
user: user,
width: 100,
height: 100
}) }}
{% if app.user is same as user and is_route_name_starts_with('user') and not is_route_name_contains('settings') %}
{% endif %}
{% endif %}
{% if stretchedLink %}
{{ user.title ?? user.username|username(false) }}
{% if (user.type) == "Service" %}
{{ 'user_badge_bot'|trans }}
{% endif %}
{% if user.isNew() %}
{% set days = constant('App\\Entity\\User::NEW_FOR_DAYS') %}
{% endif %}
{% if user.isCakeDay() %}
{% endif %}
{% if user.admin() %}
{{ 'user_badge_admin'|trans }}
{% elseif user.moderator() %}
{{ 'user_badge_global_moderator'|trans }}
{% endif %}
{% else %}
{{ user.title ?? user.apPreferredUsername ?? user.username|username(false) }}
{% if (user.type) == "Service" %}
{{ 'user_badge_bot'|trans }}
{% endif %}
{% if user.isNew() %}
{% set days = constant('App\\Entity\\User::NEW_FOR_DAYS') %}
{% endif %}
{% if user.isCakeDay() %}
{% endif %}
{% if user.admin() %}
{{ 'user_badge_admin'|trans }}
{% elseif user.moderator() %}
{{ 'user_badge_global_moderator'|trans }}
{% endif %}
{% endif %}
{{ user.username|username(true) }}
{% if user.apManuallyApprovesFollowers is same as true %}
{% endif %}
{{ component('user_actions', {user: user}) }}
{% if app.user is defined and app.user is not same as null and app.user is not same as user %}
{{ component('notification_switch', {target: user}) }}
{% endif %}
{% if user.about|length %}
{{ user.about|markdown|raw }}
{% endif %}
================================================
FILE: templates/components/user_form_actions.html.twig
================================================
================================================
FILE: templates/components/user_image_component.html.twig
================================================
{% if user.avatar and showAvatar %}
{% endif %}
================================================
FILE: templates/components/user_inline.html.twig
================================================
{% if user.avatar and showAvatar %}
{% endif %}
{{ user.title ?? user.apPreferredUsername ?? user.username|username -}}
{%- if fullName is defined and fullName is same as true -%}
@{{- user.username|apDomain -}}
{%- endif -%}
{% if user.isNew() and showNewIcon %}
{% set days = constant('App\\Entity\\User::NEW_FOR_DAYS') %}
{% endif %}
{% if user.isCakeDay() %}
{% endif %}
================================================
FILE: templates/components/user_inline_box.html.twig
================================================
{% if user.cover %}
{% endif %}
{% if user.about|length %}
{{ user.about|markdown|raw }}
{% endif %}
================================================
FILE: templates/components/user_inline_md.html.twig
================================================
{% if rich is same as true %}
{{ component('user_inline', {
user: user,
showAvatar: showAvatar,
showNewIcon: showNewIcon,
fullName: fullName,
}) }}
{% else %}
@{{- user.title ?? user.username|username -}}
{%- if fullName -%}
@{{- user.username|apDomain -}}
{%- endif -%}
{% endif %}
================================================
FILE: templates/components/vote.html.twig
================================================
{%- set VOTE_NONE = constant('App\\Entity\\Contracts\\VotableInterface::VOTE_NONE') -%}
{%- set VOTE_UP = constant('App\\Entity\\Contracts\\VotableInterface::VOTE_UP') -%}
{%- set VOTE_DOWN = constant('App\\Entity\\Contracts\\VotableInterface::VOTE_DOWN') -%}
{%- set DOWNVOTES_HIDDEN = constant('App\\Utils\\DownvotesMode::Hidden') %}
{%- set DOWNVOTES_DISABLED = constant('App\\Utils\\DownvotesMode::Disabled') %}
{% if app.user %}
{%- set user_choice = is_granted('ROLE_USER') ? subject.userChoice(app.user) : null -%}
{% set upUrl = path(formDest~'_favourite', {id: subject.id, choice: VOTE_UP}) %}
{% set downUrl = path(formDest~'_vote', {id: subject.id, choice: VOTE_DOWN}) %}
{% if(user_choice is same as(VOTE_UP)) %}
{% set choice = VOTE_UP %}
{% elseif(user_choice is same as(VOTE_DOWN)) %}
{% set choice = VOTE_DOWN %}
{% else %}
{% set choice = VOTE_NONE %}
{% endif %}
{% else %}
{% set choice = VOTE_NONE %}
{% set upUrl = path(formDest~'_favourite', {id: subject.id, choice: VOTE_NONE}) %}
{% set downUrl = path(formDest~'_vote', {id: subject.id, choice: VOTE_NONE}) %}
{% endif %}
{% set downvoteMode = mbin_downvotes_mode() %}
{% if showDownvote and downvoteMode is not same as DOWNVOTES_DISABLED %}
{% endif %}
================================================
FILE: templates/components/voters_inline.html.twig
================================================
{% if voters|length %}
{% endif %}
================================================
FILE: templates/content/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set INFINITE_SCROLL = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_INFINITE_SCROLL'), V_FALSE) -%}
{%- set SHOW_COMMENTS_AVATAR = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR')) -%}
{%- set SHOW_POST_AVATAR = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_USERS_AVATARS')) -%}
{% if criteria is defined and criteria.content is same as constant('APP\\Repository\\Criteria::CONTENT_THREADS') %}
{% set data_action = DYNAMIC_LISTS is same as V_TRUE ? 'notifications:EntryCreatedNotification@window->subject-list#addMainSubject' : 'notifications:EntryCreatedNotification@window->subject-list#increaseCounter' %}
{% elseif criteria is defined and criteria.content is same as constant('APP\\Repository\\Criteria::CONTENT_MICROBLOG') %}
{% set data_action = DYNAMIC_LISTS is same as V_TRUE ? 'notifications:PostCreatedNotification@window->subject-list#addMainSubject' : 'notifications:PostCreatedNotification@window->subject-list#increaseCounter' %}
{% else %}
{% set data_action = DYNAMIC_LISTS is same as V_TRUE ?
'notifications:EntryCreatedNotification@window->subject-list#addMainSubject notifications:PostCreatedNotification@window->subject-list#addMainSubject' :
'notifications:EntryCreatedNotification@window->subject-list#increaseCounter notifications:PostCreatedNotification@window->subject-list#increaseCounter'
%}
{% endif %}
================================================
FILE: templates/content/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{%- if magazine is defined and magazine -%}
{% if get_active_sort_option('sortBy') is not same as get_default_sort_option() %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ magazine.title }} - {{ parent() -}}
{% else %}
{{- magazine.title }} - {{ parent() -}}
{% endif %}
{%- else -%}
{% if criteria.getOption('content') == 'threads' %}
{% if get_active_sort_option('sortBy') is not same as get_default_sort_option() %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ 'thread'|trans }} - {{ parent() }}
{% else %}
{{- 'thread'|trans }} - {{ parent() -}}
{% endif %}
{% elseif criteria.getOption('content') == 'microblog' %}
{% if get_active_sort_option('sortBy') is not same as get_default_sort_option() %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ 'microblog'|trans }} - {{ parent() }}
{% else %}
{{- 'microblog'|trans }} - {{ parent() -}}
{% endif %}
{% else %}
{% if get_active_sort_option('sortBy') is not same as get_default_sort_option() %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ parent() }} - {{ kbin_meta_description() -}}
{% else %}
{{- parent() }} - {{ kbin_meta_description() -}}
{% endif %}
{% endif %}
{%- endif -%}
{%- endblock -%}
{% block description %}
{%- if magazine is defined and magazine -%}
{{- magazine.description ? get_short_sentence(magazine.description) : '' -}}
{%- else -%}
{{- parent() -}}
{%- endif -%}
{% endblock %}
{% block image %}
{%- if magazine is defined and magazine and magazine.icon -%}
{{- uploaded_asset(magazine.icon) -}}
{%- else -%}
{{- parent() -}}
{%- endif -%}
{% endblock %}
{% block mainClass %}page-entry-front{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% if magazine is defined and magazine %}
{{ magazine.title }}
{{ get_active_sort_option()|trans }}
{% if magazine.banner is not same as null %}
{% endif %}
{% else %}
{{ get_active_sort_option()|trans }}
{% endif %}
{% if criteria is defined %}
{% if criteria.getOption('content') == 'microblog' %}
{% include 'post/_form_post.html.twig' %}
{% include 'post/_options.html.twig' %}
{% else %}
{% include 'entry/_options.html.twig' %}
{% endif %}
{% endif %}
{% include 'layout/_flash.html.twig' %}
{% if magazine is defined and magazine %}
{% include 'magazine/_restricted_info.html.twig' %}
{% include 'magazine/_federated_info.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{% endif %}
{{ include('content/_list.html.twig') }}
{% endblock %}
================================================
FILE: templates/domain/_header_nav.html.twig
================================================
{{ 'threads'|trans }}
{{ 'comments'|trans }}
================================================
FILE: templates/domain/_list.html.twig
================================================
{% if domains|length %}
{{ 'name'|trans }}
{{ 'threads'|trans }}
{% for domain in domains %}
{{ domain.name }}
{{ domain.entries|length }}
{{ component('domain_sub', {domain: domain}) }}
{% endfor %}
{% else %}
{% endif %}
================================================
FILE: templates/domain/_options.html.twig
================================================
{% set showFilterLabels = app.request.cookies.get('kbin_general_filter_labels')|default('on') %}
{{ criteria.getOption('sort')|trans }}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('time') != 'all') %}
{{ criteria.getOption('time')|trans }}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('type') != 'all') %}
{{ criteria.getOption('type')|trans }}
{% endif %}
================================================
FILE: templates/domain/comment/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
title
{%- endblock -%}
{% block mainClass %}page-domain-comments-front{% endblock %}
{% block header_nav %}
{% include 'domain/_header_nav.html.twig' %}
{% endblock %}
{% block sidebar_top %}
{{ component('domain', {domain: domain}) }}
{% endblock %}
{% block body %}
{{ domain.name }}
{{ get_active_sort_option_for_comments()|trans }}
{% include 'entry/comment/_options.html.twig' %}
{% endblock %}
================================================
FILE: templates/domain/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{ domain.name }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-domain-entry-front{% endblock %}
{% block header_nav %}
{% include 'domain/_header_nav.html.twig' %}
{% endblock %}
{% block sidebar_top %}
{{ component('domain', {domain: domain}) }}
{% endblock %}
{% block body %}
{{ domain.name }}
{{ get_active_sort_option()|trans }}
{% include 'domain/_options.html.twig' %}
{% include 'entry/_list.html.twig' %}
{% endblock %}
================================================
FILE: templates/entry/_create_options.html.twig
================================================
================================================
FILE: templates/entry/_form_edit.html.twig
================================================
{% form_theme form.lang 'form/lang_select.html.twig' %}
{{ form_start(form, {attr: {class: 'entry_edit'}}) }}
{% set label %}
URL
Loading...
{% endset %}
{{ form_label(form.url, label, {'label_html': true}) }}
{{ form_errors(form.url) }}
{{ form_widget(form.url, {attr: {'data-action': 'entry-link-create#fetchLink', 'data-entry-link-create-target': 'url'}}) }}
{{ form_row(form.title, {
label: 'title', attr: {
'data-controller' : "input-length autogrow",
'data-entry-link-create-target': 'title',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\Entity\\Entry::MAX_TITLE_LENGTH')
}}) }}
{{ component('editor_toolbar', {id: 'entry_edit_body'}) }}
{{ form_row(form.body, {
label: false, attr: {
placeholder: 'body',
'data-controller': 'rich-textarea input-length autogrow',
'data-entry-link-create-target': 'description',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\Entity\\Entry::MAX_BODY_LENGTH')
}}) }}
{{ form_row(form.magazine, {label: false}) }}
{{ form_row(form.tags, {label: 'tags'}) }}
{# form_row(form.badges, {label: 'badges'}) #}
{{ form_row(form.isAdult, {label: 'is_adult', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.isOc, {label: 'oc', row_attr: {class: 'checkbox'}}) }}
{% if entry.image is not same as null %}
{% endif %}
{{ form_row(form.lang, {label: false}) }}
{{ form_row(form.submit, {label: 'edit_entry', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/entry/_form_entry.html.twig
================================================
{% form_theme form.lang 'form/lang_select.html.twig' %}
{% set hasImage = false %}
{% if edit is not defined %}
{% set edit = false %}
{% elseif entry.image %}
{% set hasImage = true %}
{% endif %}
{{ form_start(form, {attr: {class: edit ? 'entry_edit' : 'entry-create'}}) }}
{% set label %}
URL
Loading...
{% endset %}
{{ form_label(form.url, label, {'label_html': true}) }}
{{ form_errors(form.url) }}
{{ form_widget(form.url, {attr: {'data-action': 'entry-link-create#fetchLink','data-entry-link-create-target': 'url'}}) }}
{{ form_row(form.title, {
label: 'title', attr: {
'data-controller' : "input-length autogrow",
'data-entry-link-create-target': 'title',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\Entity\\Entry::MAX_TITLE_LENGTH')
}})
}}
{{ component('editor_toolbar', {id: 'entry_body'}) }}
{{ form_row(form.body, {
label: false, attr: {
placeholder: 'body',
'data-controller': 'rich-textarea input-length autogrow',
'data-entry-link-create-target': 'description',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\Entity\\Entry::MAX_BODY_LENGTH')
}})
}}
{{ form_row(form.magazine, {label: false}) }}
{{ form_row(form.tags, {label: 'tags'}) }}
{# form_row(form.badges, {label: 'badges'}) #}
{{ form_row(form.isAdult, {label: 'is_adult', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.isOc, {label: 'oc', row_attr: {class: 'checkbox'}}) }}
{% if hasImage %}
{% endif %}
{{ form_row(form.lang, {label: false}) }}
{{ form_row(form.submit, {label: edit ? 'edit_article' : 'add_new_article', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/entry/_info.html.twig
================================================
{{ 'thread'|trans }}
{% if entry.user.avatar %}
{% endif %}
{{ entry.user.username|username(true) }}
{% if entry.user.apManuallyApprovesFollowers is same as true %}
{% endif %}
{% if entry.user.apProfileId %}
{% endif %}
{{ component('user_actions', {user: entry.user}) }}
{% if app.user is defined and app.user is not same as null and app.user is not same as entry.user %}
{{ component('notification_switch', {target: entry.user}) }}
{% endif %}
{{ 'added'|trans }}: {{ component('date', {date: entry.createdAt}) }}
{% if entry.editedAt %}
{{ 'edited'|trans }}: {{ component('date', {date: entry.editedAt}) }}
{% endif %}
{% if entry.hashtags is not empty %}
{{ 'tags'|trans }}
{% endif %}
================================================
FILE: templates/entry/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set INFINITE_SCROLL = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_INFINITE_SCROLL'), V_FALSE) -%}
{% for entry in entries %}
{{ component('entry', {
entry: entry,
showMagazineName: magazine is not defined or not magazine
}) }}
{% endfor %}
{% if(entries.haveToPaginate is defined and entries.haveToPaginate) %}
{% if INFINITE_SCROLL is same as V_TRUE %}
{% else %}
{{ pagerfanta(entries, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endif %}
{% if not entries|length %}
{% if magazine is defined and magazine.postCount > 0 %}
{% endif %}
{% endif %}
================================================
FILE: templates/entry/_menu.html.twig
================================================
{{ 'more'|trans }}
================================================
FILE: templates/entry/_moderate_panel.html.twig
================================================
{% if is_granted('purge', entry) %}
{% endif %}
{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% endif %}
{{ form_start(form, {action: path('entry_change_lang', {magazine_name: magazine.name, entry_id: entry.id})}) }}
{{ form_row(form.lang, {label: false, row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.submit, {label: 'change_language'|trans, attr: {class: 'btn btn__secondary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/entry/_options.html.twig
================================================
{% set showFilterLabels = app.request.cookies.get('kbin_general_filter_labels')|default('on') %}
{{ criteria.getOption('sort')|trans }}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('time') != 'all') %}
{{ criteria.getOption('time')|trans }}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('type') != 'all') %}
{{ criteria.getOption('type')|trans }}
{% endif %}
{% if app.user %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and (criteria.favourite or criteria.moderated or criteria.subscribed)) %}
{{ criteria.resolveSubscriptionFilter()|trans }}
{% endif %}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('federation') != 'all') %}
{{ criteria.federation|trans }}
{% endif %}
================================================
FILE: templates/entry/_options_activity.html.twig
================================================
{%- set downvoteMode = mbin_downvotes_mode() %}
{%- set DOWNVOTES_ENABLED = constant('App\\Utils\\DownvotesMode::Enabled') %}
================================================
FILE: templates/entry/comment/_form_comment.html.twig
================================================
{% form_theme form.lang 'form/lang_select.html.twig' %}
{% set hasImage = false %}
{% if comment is defined and comment is not null and comment.image %}
{% set hasImage = true %}
{% endif %}
{% if edit is not defined %}
{% set edit = false %}
{% endif %}
{% if edit %}
{% set title = 'edit_comment'|trans %}
{% set action = path('entry_comment_edit', {magazine_name: entry.magazine.name, entry_id: entry.id, comment_id: comment.id}) %}
{% else %}
{% set title = 'add_comment'|trans %}
{% set action = path('entry_comment_create', {magazine_name: entry.magazine.name, entry_id: entry.id, parent_comment_id: parent is defined and parent ? parent.id : null}) %}
{% endif %}
{{ title }}
{{ form_start(form, {action: action, attr: {class: edit ? 'comment-edit replace' : 'comment-add'}}) }}
{{ component('editor_toolbar', {id: form.body.vars.id}) }}
{{ form_row(form.body, {label: false, attr: {
'data-controller': 'input-length rich-textarea autogrow',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\DTO\\EntryCommentDto::MAX_BODY_LENGTH')
}}) }}
{% if hasImage %}
{% endif %}
{{ form_row(form.lang, {label: false}) }}
{{ form_row(form.submit, {label: edit ? 'update_comment' : 'add_comment' , attr: {class: 'btn btn__primary', 'data-action': 'subject#sendForm'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/entry/comment/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set V_CHAT = constant('App\\Controller\\User\\ThemeSettingsController::CHAT') -%}
{%- set V_TREE = constant('App\\Controller\\User\\ThemeSettingsController::TREE') -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set SHOW_COMMENT_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR'), V_TRUE) -%}
{%- set VIEW_STYLE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::ENTRY_COMMENTS_VIEW'), V_TREE) -%}
{% if showNested is not defined %}
{% if VIEW_STYLE is same as V_CHAT %}
{% set showNested = false %}
{% else %}
{% set showNested = true %}
{% endif %}
{% endif %}
{% set autoAction = 'notifications:EntryCommentCreatedNotification@window->subject-list#addComment' %}
{% set manualAction = 'notifications:EntryCommentCreatedNotification@window->subject-list#increaseCounter' %}
================================================
FILE: templates/entry/comment/_menu.html.twig
================================================
{{ 'more'|trans }}
================================================
FILE: templates/entry/comment/_moderate_panel.html.twig
================================================
{% if is_granted('purge', comment) %}
{% endif %}
{{ form_start(form, {action: path('entry_comment_change_lang', {magazine_name: magazine.name, entry_id: entry.id, comment_id: comment.id})}) }}
{{ form_row(form.lang, {label: false, row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.submit, {label: 'change_language'|trans, attr: {class: 'btn btn__secondary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/entry/comment/_no_comments.html.twig
================================================
================================================
FILE: templates/entry/comment/_options.html.twig
================================================
{% set showFilterLabels = app.request.cookies.get('kbin_general_filter_labels')|default('on') %}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::ENTRY_COMMENTS_VIEW')) is not same as constant('App\\Controller\\User\\ThemeSettingsController::CHAT') %}
{{ criteria.getOption('sort')|trans }}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('time') != 'all') %}
{{ criteria.getOption('time')|trans }}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('federation') != 'all') %}
{{ criteria.federation|trans }}
{% endif %}
{% else %}
{{ 'comments'|trans }}
{% endif %}
================================================
FILE: templates/entry/comment/_options_activity.html.twig
================================================
{% set downvoteMode = mbin_downvotes_mode() %}
{%- set DOWNVOTES_ENABLED = constant('App\\Utils\\DownvotesMode::Enabled') %}
================================================
FILE: templates/entry/comment/create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'add_comment'|trans }} - {{ entry.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-comment-create{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {
entry: entry,
isSingle: true,
showShortSentence: false,
showBody:false
}) }}
{% if parent is defined and parent %}
{{ component('entry_comment', {
comment: parent,
showEntryTitle: false,
showNested: false
}) }}
{% endif %}
{% include 'layout/_flash.html.twig' %}
{% if user.visibility is same as 'visible' %}
{% include 'entry/comment/_form_comment.html.twig' %}
{% endif %}
{% endblock %}
================================================
FILE: templates/entry/comment/edit.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'edit_comment'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-comment-edit{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry_comment', {
comment: comment,
dateAsUrl: false,
showEntryTitle: false,
showMagazineName: false,
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/comment/_form_comment.html.twig' with {edit: true} %}
{% endblock %}
================================================
FILE: templates/entry/comment/favourites.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'favourites'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-comment-favourites{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry_comment', {
comment: comment,
showEntryTitle: false,
showMagazineName: false
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/comment/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: favourites} %}
{% endblock %}
================================================
FILE: templates/entry/comment/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{% if magazine is defined and magazine %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ 'comments'|trans }} - {{ magazine.title }} - {{ parent() -}}
{% else %}
{{- get_active_sort_option('sortBy')|trans|capitalize }} - {{ 'comments'|trans }} - {{ parent() -}}
{% endif %}
{%- endblock -%}
{% block mainClass %}page-entry-comments-front{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% if magazine is defined and magazine %}
{{ magazine.title }}
{{ get_active_sort_option()|trans }}
{% else %}
{{ get_active_sort_option()|trans }}
{% endif %}
{% include 'entry/comment/_options.html.twig' %}
{% include 'layout/_flash.html.twig' %}
{% if magazine is defined and magazine %}
{% include 'magazine/_federated_info.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{% endif %}
{% endblock %}
================================================
FILE: templates/entry/comment/moderate.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderate'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-moderate{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry_comment', {
comment: comment,
dateAsUrl: false,
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/comment/_moderate_panel.html.twig' %}
{% endblock %}
================================================
FILE: templates/entry/comment/view.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set SHOW_COMMENT_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR'), V_TRUE) -%}
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'comments'|trans }} - {{ entry.title }} - {{ parent() -}}
{%- endblock -%}
{% block stylesheets %}
{{ parent() }}
{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {
entry: entry,
isSingle: true,
showShortSentence: false,
showBody:false
}) }}
{% set autoAction = 'notifications:EntryCommentCreatedNotification@window->subject-list#addComment' %}
{% set manualAction = 'notifications:EntryCommentCreatedNotification@window->subject-list#increaseCounter' %}
{% endblock %}
================================================
FILE: templates/entry/comment/voters.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'activity'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-comment-voters{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry_comment', {
comment: comment,
showEntryTitle: false,
showMagazineName: false
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/comment/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: votes} %}
{% endblock %}
================================================
FILE: templates/entry/create_entry.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'add_new_article'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-create page-entry-create-article{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'entry/_create_options.html.twig' %}
{{ 'add_new_article'|trans }}
{% include 'layout/_flash.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% if user.visibility is same as 'visible' %}
{% include 'entry/_form_entry.html.twig' %}
{% endif %}
{% endblock %}
================================================
FILE: templates/entry/edit_entry.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'edit_entry'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-create page-entry-edit-article{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/_form_edit.html.twig' %}
{% endblock %}
================================================
FILE: templates/entry/favourites.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'favourites'|trans }} - {{ entry.title}} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-favourites{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {
entry: entry,
isSingle: true,
showBody: false
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: favourites} %}
{% endblock %}
================================================
FILE: templates/entry/moderate.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderate'|trans }} - {{ entry.title }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-moderate{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {
entry: entry,
isSingle: true,
showShortSentence: false,
showBody:true,
moderate:true,
class: 'section--top'
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/_moderate_panel.html.twig' %}
{% endblock %}
================================================
FILE: templates/entry/single.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- entry.title }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block description %}
{{- entry.body ? get_short_sentence(entry.body) : '' -}}
{% endblock %}
{% block image %}
{%- if entry.image -%}
{{- uploaded_asset(entry.image) -}}
{%- elseif entry.magazine.icon -%}
{{- uploaded_asset(entry.magazine.icon) -}}
{%- else -%}
{{- parent() -}}
{%- endif -%}
{% endblock %}
{% block mainClass %}page-entry-single{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {
entry: entry,
isSingle: true,
showShortSentence: false,
showBody:true
}) }}
{{ component('entries_cross', {entry: entry}) }}
{% include 'layout/_flash.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% if user is defined and user and user.visibility is same as 'visible' and not entry.isLocked and (user_settings.comment_reply_position == constant('App\\Controller\\User\\ThemeSettingsController::TOP')) %}
{% endif %}
{% include 'entry/comment/_options.html.twig' %}
{% if entry.isLocked %}
{{ 'comments_locked'|trans }}
{% endif %}
{% if user is defined and user and user.visibility is same as 'visible' and not entry.isLocked and (user_settings.comment_reply_position == constant('App\\Controller\\User\\ThemeSettingsController::BOTTOM')) %}
{% endif %}
{% include 'entry/_options_activity.html.twig' %}
{% endblock %}
================================================
FILE: templates/entry/voters.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- route_has_param('type', 'up') ? 'up_votes'|trans : 'down_votes'|trans }} - {{ entry.title}} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-voters{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('entry', {entry: entry, isSingle: true, showBody: false}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'entry/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: votes} %}
{% endblock %}
================================================
FILE: templates/form/lang_select.html.twig
================================================
{# This block needed as the default one (of which this is a very close copy) does not respect preferred_choices like it should #}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
{% set options = choice %}
{{- block('choice_widget_options') -}}
{%- elseif render_preferred_choices|default(false) or (not render_preferred_choices|default(false) and choice not in preferred_choices) -%}
{{ choice.label }}
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
================================================
FILE: templates/layout/_domain_activity_list.html.twig
================================================
{% if actor is not defined %}
{% set actor = 'magazine' %}
{% endif %}
{% if list|length %}
{% for subject in list %}
{% endfor %}
{% if(list.haveToPaginate is defined and list.haveToPaginate) %}
{{ pagerfanta(list, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
================================================
FILE: templates/layout/_flash.html.twig
================================================
{% for flash_error in app.flashes('error') %}
{{ flash_error|trans }}
{% endfor %}
{% for flash_success in app.flashes('success') %}
{{ flash_success|trans }}
{% endfor %}
================================================
FILE: templates/layout/_form_media.html.twig
================================================
================================================
FILE: templates/layout/_generic_subject_list.html.twig
================================================
{{ include('layout/_subject_list.html.twig') }}
================================================
FILE: templates/layout/_header.html.twig
================================================
{%- set STATUS_PENDING = constant('App\\Entity\\Report::STATUS_PENDING') -%}
================================================
FILE: templates/layout/_header_bread.html.twig
================================================
{% set filter_option = criteria is defined ? criteria.getOption('subscription') : null %}
{% if magazine is defined and magazine %}
{% elseif is_route_name_starts_with('domain_') %}
{% elseif tag is defined and tag %}
{% elseif filter_option == 'subscribed' or is_route_name_end_with('_subscribed') %}
{% elseif filter_option == 'favourites' or is_route_name_end_with('_favourite') %}
{% elseif filter_option == 'moderated' or is_route_name_end_with('_moderated') %}
{% endif %}
================================================
FILE: templates/layout/_header_nav.html.twig
================================================
{% set activeLink = '' %}
{% if (is_route_name_contains('people') or is_route_name_starts_with('user')) and not is_route_name_contains('settings') %}
{% set activeLink = 'people' %}
{% elseif is_route_name('magazine_list_all') %}
{% set activeLink = 'magazines' %}
{% elseif criteria is defined %}
{% if criteria.getOption('content') == 'threads' %}
{% set activeLink = 'threads' %}
{% elseif criteria.getOption('content') == 'microblog' %}
{% set activeLink = 'microblog' %}
{% elseif criteria.getOption('content') == 'combined' %}
{% set activeLink = 'combined' %}
{% endif %}
{% elseif entry is defined and entry %}
{% set activeLink = 'threads' %}
{% elseif post is defined and post %}
{% set activeLink = 'microblog' %}
{% endif %}
{% if header_nav is empty %}
{{ 'combined'|trans }}
{{ 'threads'|trans }} {% if magazine is defined and magazine %}({{ magazine.entryCount }}){% endif %}
{{ 'microblog'|trans }} {% if magazine is defined and magazine %}({{ magazine.postCount }}){% endif %}
{{ 'people'|trans }}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_TOPBAR')) is not same as 'true' %}
{{ 'magazines'|trans }}
{% endif %}
{% else %}
{{ header_nav|raw }}
{% endif %}
================================================
FILE: templates/layout/_magazine_activity_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if actor is not defined %}
{% set actor = 'magazine' %}
{% endif %}
{% if list|length %}
{% for subject in list %}
{% if attribute(subject, actor).icon and attribute(subject, actor).icon.filePath and (app.user or attribute(subject, actor).isAdult is same as false) %}
{% endif %}
{% endfor %}
{% if(list.haveToPaginate is defined and list.haveToPaginate) %}
{{ pagerfanta(list, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
================================================
FILE: templates/layout/_options_appearance.html.twig
================================================
{{ 'reload_to_apply'|trans }}
{{ 'general'|trans }}
{{ component('settings_row_enum', {label: 'sidebar_position'|trans, settingsKey: 'KBIN_GENERAL_SIDEBAR_POSITION', values: [ {name: 'left'|trans , value: 'LEFT'}, {name: 'right'|trans , value: 'RIGHT' } ], defaultValue: 'RIGHT' } ) }}
{{ component('settings_row_enum', {label: 'page_width'|trans, settingsKey: 'KBIN_PAGE_WIDTH', values: [ {name: 'page_width_max'|trans , value: 'MAX'}, {name: 'page_width_auto'|trans , value: 'AUTO' }, {name: 'page_width_fixed'|trans , value: 'FIXED' } ], defaultValue: 'FIXED', class: 'width-setting' } ) }}
{{ component('settings_row_enum', {label: 'filter_labels'|trans, settingsKey: 'KBIN_GENERAL_FILTER_LABELS', values: [ {name: 'on'|trans , value: 'ON'}, {name: 'auto'|trans , value: 'AUTO' }, {name: 'off'|trans , value: 'OFF' } ], defaultValue: 'ON' } ) }}
{% if kbin_mercure_enabled() %}
{{ component('settings_row_switch', {label: 'dynamic_lists'|trans, settingsKey: 'KBIN_GENERAL_DYNAMIC_LISTS'}) }}
{% endif %}
{{ component('settings_row_switch', {label: 'rounded_edges'|trans, settingsKey: 'KBIN_GENERAL_ROUNDED_EDGES', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'infinite_scroll'|trans, help: 'infinite_scroll_help'|trans , settingsKey: 'KBIN_GENERAL_INFINITE_SCROLL'}) }}
{{ component('settings_row_switch', {label: 'sticky_navbar'|trans, help: 'sticky_navbar_help'|trans, settingsKey: 'KBIN_GENERAL_FIXED_NAVBAR'}) }}
{{ component('settings_row_switch', {label: 'show_top_bar'|trans, settingsKey: 'KBIN_GENERAL_TOPBAR'}) }}
{{ component('settings_row_switch', {label: 'show_related_magazines'|trans, settingsKey: 'MBIN_GENERAL_SHOW_RELATED_MAGAZINES', defaultValue: 'true'}) }}
{{ component('settings_row_switch', {label: 'show_related_entries'|trans, settingsKey: 'MBIN_GENERAL_SHOW_RELATED_ENTRIES', defaultValue: 'true'}) }}
{{ component('settings_row_switch', {label: 'show_related_posts'|trans, settingsKey: 'MBIN_GENERAL_SHOW_RELATED_POSTS', defaultValue: 'true'}) }}
{{ component('settings_row_switch', {label: 'show_active_users'|trans, settingsKey: 'MBIN_GENERAL_SHOW_ACTIVE_USERS', defaultValue: 'true'}) }}
{{ component('settings_row_switch', {label: 'show_user_domains'|trans, settingsKey: 'MBIN_SHOW_USER_DOMAIN', defaultValue: false}) }}
{{ component('settings_row_switch', {label: 'show_magazine_domains'|trans, settingsKey: 'MBIN_SHOW_MAGAZINE_DOMAIN', defaultValue: false}) }}
{% if app.user is defined and app.user is not same as null %}
{{ 'subscriptions'|trans }}
{{ component('settings_row_switch', {label: 'show_subscriptions'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_SHOW', defaultValue: 'true'}) }}
{{ component('settings_row_switch', {label: 'show_magazines_icons'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_SHOW_MAGAZINE_ICON', defaultValue: 'true'}) }}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SHOW')) is not same as 'false' %}
{{ component('settings_row_enum', {label: 'subscription_sort'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_SORT', values: [ {name: 'alphabetically'|trans , value: 'ALPHABETICALLY'}, {name: 'last_active'|trans , value: 'LAST_ACTIVE' } ], defaultValue: 'LAST_ACTIVE'}) }}
{{ component('settings_row_switch', {label: 'subscriptions_in_own_sidebar'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR'}) }}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR')) is same as 'true' %}
{{ component('settings_row_switch', {label: 'sidebars_same_side'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_SIDEBARS_SAME_SIDE'}) }}
{% else %}
{{ component('settings_row_switch', {label: 'subscription_panel_large'|trans, settingsKey: 'KBIN_SUBSCRIPTIONS_LARGE_PANEL'}) }}
{% endif %}
{% endif %}
{% endif %}
{{ 'threads'|trans }}
{{ component('settings_row_switch', {label: 'auto_preview'|trans, help: 'auto_preview_help'|trans, settingsKey: 'KBIN_ENTRIES_SHOW_PREVIEW'}) }}
{{ component('settings_row_switch', {label: 'compact_view'|trans, help: 'compact_view_help'|trans, settingsKey: 'KBIN_ENTRIES_COMPACT'}) }}
{{ component('settings_row_switch', {label: 'show_users_avatars'|trans, help: 'show_users_avatars_help'|trans, settingsKey: 'KBIN_ENTRIES_SHOW_USERS_AVATARS', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_magazines_icons'|trans, help: 'show_magazines_icons_help'|trans, settingsKey: 'KBIN_ENTRIES_SHOW_MAGAZINES_ICONS', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_thumbnails'|trans, help: 'show_thumbnails_help'|trans, settingsKey: 'KBIN_ENTRIES_SHOW_THUMBNAILS', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'image_lightbox_in_list'|trans, help: 'image_lightbox_in_list_help'|trans, settingsKey: 'MBIN_LIST_IMAGE_LIGHTBOX', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_rich_mention'|trans, help: 'show_rich_mention_help'|trans, settingsKey: 'MBIN_ENTRIES_SHOW_RICH_MENTION', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_rich_mention_magazine'|trans, help: 'show_rich_mention_magazine_help'|trans, settingsKey: 'MBIN_ENTRIES_SHOW_RICH_MENTION_MAGAZINE', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_rich_ap_link'|trans, help: 'show_rich_ap_link_help'|trans, settingsKey: 'MBIN_ENTRIES_SHOW_RICH_AP_LINK', defaultValue: true}) }}
{{ 'microblog'|trans }}
{{ component('settings_row_switch', {label: 'auto_preview'|trans, help: 'auto_preview_help'|trans, settingsKey: 'KBIN_POSTS_SHOW_PREVIEW'}) }}
{{ component('settings_row_switch', {label: 'show_users_avatars'|trans, help: 'show_users_avatars_help'|trans, settingsKey: 'KBIN_POSTS_SHOW_USERS_AVATARS', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_rich_mention'|trans, help: 'show_rich_mention_help'|trans, settingsKey: 'MBIN_POSTS_SHOW_RICH_MENTION', defaultValue: false}) }}
{{ component('settings_row_switch', {label: 'show_rich_mention_magazine'|trans, help: 'show_rich_mention_magazine_help'|trans, settingsKey: 'MBIN_POSTS_SHOW_RICH_MENTION_MAGAZINE', defaultValue: true}) }}
{{ component('settings_row_switch', {label: 'show_rich_ap_link'|trans, help: 'show_rich_ap_link_help'|trans, settingsKey: 'MBIN_POSTS_SHOW_RICH_AP_LINK', defaultValue: true}) }}
{{ 'single_settings'|trans }}
{{ component('settings_row_enum', {label: 'comment_reply_position'|trans, help: 'comment_reply_position_help'|trans, settingsKey: 'KBIN_COMMENTS_REPLY_POSITION', values: [ {name: 'position_top'|trans , value: 'TOP'}, {name: 'position_bottom'|trans , value: 'BOTTOM' } ], defaultValue: 'TOP' } ) }}
{{ component('settings_row_switch', {label: 'show_avatars_on_comments'|trans, help: 'show_avatars_on_comments_help'|trans, settingsKey: 'KBIN_COMMENTS_SHOW_USER_AVATAR', defaultValue: true}) }}
{{ 'mod_log'|trans }}
{{ component('settings_row_switch', {label: 'show_users_avatars'|trans, help: 'show_users_avatars_help'|trans, settingsKey: 'MBIN_MODERATION_LOG_SHOW_USER_AVATARS', defaultValue: false}) }}
{{ component('settings_row_switch', {label: 'show_magazines_icons'|trans, help: 'show_magazines_icons_help'|trans, settingsKey: 'MBIN_MODERATION_LOG_SHOW_MAGAZINE_ICONS', defaultValue: false}) }}
{{ component('settings_row_switch', {label: 'show_new_icons'|trans, help: 'show_new_icons_help'|trans, settingsKey: 'MBIN_MODERATION_LOG_SHOW_NEW_ICONS', defaultValue: true}) }}
================================================
FILE: templates/layout/_options_font_size.html.twig
================================================
================================================
FILE: templates/layout/_options_theme.html.twig
================================================
{% set theme = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_THEME')) %}
{% set theme_key = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_THEME') %}
================================================
FILE: templates/layout/_pagination.html.twig
================================================
{%- block pager_widget -%}
{%- endblock pager_widget -%}
{%- block pager -%}
{# Previous Page Link #}
{%- if pagerfanta.hasPreviousPage() -%}
{%- set path = route_generator.route(pagerfanta.getPreviousPage()) -%}
{{- block('previous_page_link') -}}
{%- else -%}
{{- block('previous_page_link_disabled') -}}
{%- endif -%}
{# First Page Link #}
{%- if start_page > 1 -%}
{%- set page = 1 -%}
{%- set path = route_generator.route(page) -%}
{{- block('page_link') -}}
{%- endif -%}
{# Second Page Link, displays if we are on page 3 #}
{%- if start_page == 3 -%}
{%- set page = 2 -%}
{%- set path = route_generator.route(page) -%}
{{- block('page_link') -}}
{%- endif -%}
{# Separator, creates a "..." separator to limit the number of items if we are starting beyond page 3 #}
{%- if start_page > 3 -%}
{{- block('ellipsis') -}}
{%- endif -%}
{# Page Links #}
{%- for page in range(start_page, end_page) -%}
{%- set path = route_generator.route(page) -%}
{%- if page == current_page -%}
{{- block('current_page_link') -}}
{%- else -%}
{{- block('page_link') -}}
{%- endif -%}
{%- endfor -%}
{# Separator, creates a "..." separator to limit the number of items if we are over 3 pages away from the last page #}
{%- if end_page < (nb_pages - 2) -%}
{{- block('ellipsis') -}}
{%- endif -%}
{# Second to Last Page Link, displays if we are on the third from last page #}
{%- if end_page == (nb_pages - 2) -%}
{%- set page = (nb_pages - 1) -%}
{%- set path = route_generator.route(page) -%}
{{- block('page_link') -}}
{%- endif -%}
{# Last Page Link #}
{%- if nb_pages > end_page -%}
{%- set page = nb_pages -%}
{%- set path = route_generator.route(page) -%}
{{- block('page_link') -}}
{%- endif -%}
{# Next Page Link #}
{%- if pagerfanta.hasNextPage() -%}
{%- set path = route_generator.route(pagerfanta.getNextPage()) -%}
{{- block('next_page_link') -}}
{%- else -%}
{{- block('next_page_link_disabled') -}}
{%- endif -%}
{%- endblock pager -%}
{%- block page_link -%}
{%- endblock page_link -%}
{%- block current_page_link -%}
{%- endblock current_page_link -%}
{%- block previous_page_link -%}
{%- endblock previous_page_link -%}
{%- block previous_page_link_disabled -%}
{%- endblock previous_page_link_disabled -%}
{%- block previous_page_message -%}
{%- if options['prev_message'] is defined -%}
{{- options['prev_message'] -}}
{%- else -%}
«
{%- endif -%}
{%- endblock previous_page_message -%}
{%- block next_page_link -%}
{%- endblock next_page_link -%}
{%- block next_page_link_disabled -%}
{%- endblock next_page_link_disabled -%}
{%- block next_page_message -%}
{%- if options['next_message'] is defined -%}
{{- options['next_message'] -}}
{%- else -%}
»
{%- endif -%}
{%- endblock next_page_message -%}
{%- block ellipsis -%}
{%- endblock ellipsis -%}
================================================
FILE: templates/layout/_sidebar.html.twig
================================================
{% set show_related_magazines = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_GENERAL_SHOW_RELATED_MAGAZINES')) %}
{% set show_related_entries = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_GENERAL_SHOW_RELATED_ENTRIES')) %}
{% set show_related_posts = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_GENERAL_SHOW_RELATED_POSTS')) %}
{% set show_active_users = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_GENERAL_SHOW_ACTIVE_USERS')) %}
{% set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') %}
{% if sidebar_top is empty %}
{% else %}
{{ sidebar_top|raw }}
{% endif %}
{% if user is defined and user and is_route_name_starts_with('user') %}
{% include 'user/_info.html.twig' %}
{% endif %}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SHOW')) is not same as 'false' and
app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR')) is not same as 'true' and
app.user is defined and app.user is not same as null %}
{{ component('sidebar_subscriptions', { openMagazine: magazine is defined ? magazine : null, user: app.user, sort: app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SORT')) }) }}
{% endif %}
{% if entry is defined and magazine %}
{% include 'entry/_info.html.twig' %}
{% endif %}
{% if post is defined and magazine %}
{% include 'post/_info.html.twig' %}
{% endif %}
{% if magazine is defined and magazine %}
{{ component('magazine_box', {
magazine: magazine,
showSectionTitle: true
}) }}
{% include 'magazine/_moderators_sidebar.html.twig' %}
{% endif %}
{% if tag is defined and tag %}
{% include 'tag/_panel.html.twig' %}
{% endif %}
{% if not is_route_name_contains('login') %}
{% if show_related_magazines is not same as V_FALSE %}
{{ component('related_magazines', {magazine: magazine is defined and magazine ? magazine.name : null, tag: tag is defined and tag ? tag : null}) }}
{% endif %}
{% if not is_route_name_contains('people') and show_active_users is not same as V_FALSE %}
{{ component('active_users', {magazine: magazine is defined and magazine ? magazine : null}) }}
{% endif %}
{% if show_related_posts is not same as V_FALSE %}
{{ component('related_posts', {magazine: magazine is defined and magazine ? magazine.name : null, tag: tag is defined and tag ? tag : null}) }}
{% endif%}
{% if show_related_entries is not same as V_FALSE %}
{{ component('related_entries', {magazine: magazine is defined and magazine ? magazine.name : null, tag: tag is defined and tag ? tag : null}) }}
{% endif%}
{% endif %}
{{ kbin_domain() }}
{{ 'about_instance'|trans }}
●
{{ 'contact'|trans }}
●
{{ 'faq'|trans }}
●
{{ 'terms'|trans }}
●
{{ 'privacy_policy'|trans }}
●
{% if kbin_federation_page_enabled() %}
{{ 'federation'|trans }}
●
{% endif %}
{{ 'mod_log'|trans }}
●
{{ 'stats'|trans }}
●
{% if magazine is defined and magazine %}
{% set args = {'magazine': magazine.name ?? '' } %}
{% elseif user is defined and user %}
{% set args = {'user': user.username ?? '' } %}
{% elseif tag is defined and tag %}
{% set args = {'tag': tag ?? '' } %}
{% elseif domain is defined and domain %}
{% set args = {'domain': domain.name ?? '' } %}
{% else %}
{% set args = {} %}
{% endif %}
{% if criteria is defined and criteria %}
{% if criteria.getOption('content') is same as 'threads' %}
{% set args = {...args, 'content': 'threads'} %}
{% elseif criteria.getOption('content') is same as 'microblog' %}
{% set args = {...args, 'content': 'microblog'} %}
{% elseif criteria.getOption('content') is same as 'combined' %}
{% set args = {...args, 'content': 'combined'} %}
{% endif %}
{% endif %}
{{ 'rss'|trans }}
{% set header_accept_language = app.request.headers.has('accept_language')
? app.request.headers.get('accept_language')|slice(0,2)
: null %}
{% set current = app.request.cookies.get('mbin_lang') ?? header_accept_language ?? kbin_default_lang() %}
{% for code in ['bg', 'ca', 'da', 'de', 'el', 'en', 'eo', 'es', 'fil', 'fr', 'gl', 'it', 'ja', 'nl', 'pl', 'pt', 'pt_BR', 'ru', 'tr', 'uk', 'zh_TW'] %}
{{ code|language_name(code) }}
{% endfor %}
================================================
FILE: templates/layout/_subject.html.twig
================================================
{% if attributes is not defined %}
{% set attributes = {} %}
{% endif %}
{% if entryCommentAttributes is not defined %}
{% set entryCommentAttributes = {} %}
{% endif %}
{% if entryAttributes is not defined %}
{% set entryAttributes = {} %}
{% endif %}
{% if postAttributes is not defined %}
{% set postAttributes = {} %}
{% endif %}
{% if postCommentAttributes is not defined %}
{% set postCommentAttributes = {} %}
{% endif %}
{% if magazineAttributes is not defined %}
{% set magazineAttributes = {} %}
{% endif %}
{% if userAttributes is not defined %}
{% set userAttributes = {} %}
{% endif %}
{% set forCombined = (route_param_exists('content') and get_route_param('content') is same as 'combined')
or (criteria is defined and criteria.getOption('content') is same as 'combined') %}
{% if subject is entry %}
{{ component('entry', {entry: subject}|merge(attributes)|merge(entryAttributes)) }}
{% elseif subject is entry_comment %}
{{ component('entry_comment', {comment: subject, showEntryTitle: forCombined is same as true}|merge(attributes)|merge(entryCommentAttributes)) }}
{% elseif subject is post %}
{% if forCombined is same as true %}
{{ component('post_combined', {post: subject}|merge(attributes)|merge(postAttributes)) }}
{% else %}
{{ component('post', {post: subject}|merge(attributes)|merge(postAttributes)) }}
{% endif %}
{% elseif subject is post_comment %}
{% if forCombined is same as true %}
{{ component('post_comment_combined', {comment: subject}|merge(attributes)|merge(postCommentAttributes)) }}
{% else %}
{{ component('post_comment', {comment: subject}|merge(attributes)|merge(postCommentAttributes)) }}
{% endif %}
{% elseif subject is magazine %}
{{ component('magazine_box', {magazine: subject}|merge(attributes, magazineAttributes)) }}
{% elseif subject is user %}
{{ component('user_inline_box', {user: subject}|merge(attributes, userAttributes)) }}
{% endif %}
================================================
FILE: templates/layout/_subject_link.html.twig
================================================
{%- if subject is entry -%}
{{ subject.shortTitle }}
{%- elseif subject is entry_comment -%}
{{ subject.shortTitle }}
{%- elseif subject is post -%}
{{ subject.shortTitle }}
{%- elseif subject is post_comment -%}
{{ subject.shortTitle }}
{%- endif -%}
================================================
FILE: templates/layout/_subject_list.html.twig
================================================
{% if attributes is not defined %}
{% set attributes = {} %}
{% endif %}
{% if entryCommentAttributes is not defined %}
{% set entryCommentAttributes = {} %}
{% endif %}
{% if entryAttributes is not defined %}
{% set entryAttributes = {} %}
{% endif %}
{% if postAttributes is not defined %}
{% set postAttributes = {} %}
{% endif %}
{% if postCommentAttributes is not defined %}
{% set postCommentAttributes = {} %}
{% endif %}
{% for subject in results %}
{% include 'layout/_subject.html.twig' %}
{% endfor %}
{% if pagination is defined and pagination %}
{% if(pagination.haveToPaginate is defined and pagination.haveToPaginate) %}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_INFINITE_SCROLL')) is same as 'true' %}
{% elseif pagination.getCurrentCursor is defined %}
{{ component('cursor_pagination', {'pagination': pagination}) }}
{% else %}
{{ pagerfanta(pagination, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endif %}
{% else %}
{% if(results.haveToPaginate is defined and results.haveToPaginate) %}
{% if app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_INFINITE_SCROLL')) is same as 'true' %}
{% elseif results.getCurrentCursor is defined %}
{{ component('cursor_pagination', {'pagination': results}) }}
{% else %}
{{ pagerfanta(results, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endif %}
{% endif %}
{% if not results|length %}
{% endif %}
================================================
FILE: templates/layout/_topbar.html.twig
================================================
{% set show_topbar = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_TOPBAR')) %}
{% set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') %}
{% if show_topbar is same as V_TRUE %}
{% endif %}
================================================
FILE: templates/layout/_user_activity_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{% if actor is not defined %}
{% set actor = 'user' %}
{% endif %}
{% if list|length %}
{% for subject in list %}
{% if attribute(subject, actor).avatar %}
{{ component('user_avatar', {user: attribute(subject, actor) }) }}
{% endif %}
{% endfor %}
{% if(list.haveToPaginate is defined and list.haveToPaginate) %}
{{ pagerfanta(list, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
================================================
FILE: templates/layout/sidebar_subscriptions.html.twig
================================================
{% with %}
{% set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') %}
{% set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') %}
{% set V_LEFT = constant('App\\Controller\\User\\ThemeSettingsController::LEFT') %}
{% set KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_IN_SEPARATE_SIDEBAR') %}
{% set KBIN_SUBSCRIPTIONS_SIDEBARS_SAME_SIDE = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SIDEBARS_SAME_SIDE') %}
{% set KBIN_GENERAL_SIDEBAR_POSITION = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_SIDEBAR_POSITION') %}
{% set KBIN_SUBSCRIPTIONS_LARGE_PANEL = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_LARGE_PANEL') %}
{% set KBIN_SUBSCRIPTIONS_SHOW_MAGAZINE_ICON = constant('App\\Controller\\User\\ThemeSettingsController::KBIN_SUBSCRIPTIONS_SHOW_MAGAZINE_ICON') %}
{% set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) %}
{# for whatever reason doing {% set TRUE = ... %} would crash #}
{% endwith %}
================================================
FILE: templates/magazine/_federated_info.html.twig
================================================
{% if magazine.apId %} {# I.e. if we're federated #}
{% if entries is defined and entries and not entries.hasNextPage %}
{# Then show a link to original if we're at the end of content #}
{% endif %}
{% if is_instance_of_magazine_blocked(magazine) %}
{{ 'magazine_instance_defederated_info'|trans }}
{% elseif not magazine_has_local_subscribers(magazine) %}
{# Also show a warning if we're not actively receiving updates #}
{% set lastOriginUpdate = magazine.lastOriginUpdate %}
{% if lastOriginUpdate is not null %}
{% set currentTime = "now"|date('U') %}
{% set secondsDifference = currentTime - (lastOriginUpdate|date('U')) %}
{% set daysDifference = (secondsDifference / 86400)|round(0, 'floor') %}
{{ 'disconnected_magazine_info'|trans({'%days%': daysDifference}) }}
{% if app.user %}
{{ 'subscribe_for_updates'|trans }}
{% endif %}
{% else %}
{{ 'always_disconnected_magazine_info'|trans }}
{% if app.user %}
{{ 'subscribe_for_updates'|trans }}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: templates/magazine/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_MAGAZINE_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_MAGAZINE_DOMAIN'), V_FALSE) -%}
{% if magazines|length %}
{% if view is same as 'cards'|trans|lower %}
{% for magazine in magazines %}
{{ component('magazine_box', {magazine: magazine, showMeta: false, showInfo: false}) }}
{% endfor %}
{% elseif view is same as 'columns'|trans|lower %}
{% for magazine in magazines %}
{% if magazine.icon and (app.user or magazine.isAdult is same as false) %}
{% endif %}
{% endfor %}
{% else %}
{% set sortBy = criteria.sortOption %}
{{ 'name'|trans }}
{% for column in ['threads', 'comments', 'posts'] %}
{% if sortBy is same as column %}
{{ column|trans }}
{% else %}
{{ column|trans }}
{% endif %}
{% endfor %}
{% if sortBy is same as 'hot' %}
{{ 'subscriptions'|trans }}
{% else %}
{{ 'subscriptions'|trans }}
{% endif %}
{% for magazine in magazines %}
{{ component('magazine_inline', { magazine: magazine, stretchedLink: true, showAvatar: true, showNewIcon: true, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }}
{% if magazine.isAdult %}18+ {% endif %}
{{ magazine.entryCount|abbreviateNumber }}
{{ magazine.entryCommentCount|abbreviateNumber }}
{{ (magazine.postCount + magazine.postCommentCount)|abbreviateNumber }}
{{ component('magazine_sub', {magazine: magazine}) }}
{% endfor %}
{% for magazine in magazines %}
{{ component('magazine_inline', { magazine: magazine, stretchedLink: true, showAvatar: true, showNewIcon: true, fullName: SHOW_MAGAZINE_FULLNAME is same as V_TRUE}) }} {% if magazine.isAdult %}18+ {% endif %}
{{ magazine.entryCount|abbreviateNumber }} {{ 'threads'|trans }}
{{ magazine.entryCommentCount|abbreviateNumber }} {{ 'comments'|trans }}
{{ (magazine.postCount + magazine.postCommentCount)|abbreviateNumber }} {{ 'posts'|trans }}
{{ component('magazine_sub', {magazine: magazine}) }}
{% endfor %}
{% endif %}
{% if(magazines.haveToPaginate is defined and magazines.haveToPaginate) %}
{{ pagerfanta(magazines, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
================================================
FILE: templates/magazine/_moderators_list.html.twig
================================================
{% for moderator in moderators %}
{% if moderator.user.avatar %}
{{ component('user_avatar', {user: moderator.user}) }}
{% endif %}
{% if is_granted('edit', magazine) and not moderator.isOwner and (magazine.apId is same as null or moderator.user.apId is same as null) %}
{% endif %}
{% endfor %}
================================================
FILE: templates/magazine/_moderators_sidebar.html.twig
================================================
{% for moderator in magazine.moderators|slice(0, 5) %}
{{ component('user_inline', { user: moderator.user, showNewIcon: true }) }}
{% endfor %}
{% if magazine.moderators|length > 5 %}
{% endif %}
================================================
FILE: templates/magazine/_options.html.twig
================================================
================================================
FILE: templates/magazine/_restricted_info.html.twig
================================================
{% if magazine.postingRestrictedToMods and (app.user is not defined or app.user is same as null or magazine.isActorPostingRestricted(app.user)) %}
{{ 'magazine_posting_restricted_to_mods_warning'|trans }}
{% endif %}
================================================
FILE: templates/magazine/_visibility_info.html.twig
================================================
{% if magazine.visibility is same as 'soft_deleted' %}
{{ 'magazine_is_deleted'|trans({
'%link_target%': path('magazine_panel_general', {'name': magazine.name})
})|raw }}
{% endif %}
================================================
FILE: templates/magazine/create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'create_new_magazine'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-entry-create page-entry-create-link{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'entry/_create_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% if user.visibility is same as 'visible' %}
{{ 'create_new_magazine'|trans }}
{{ form_start(form) }}
{{ form_row(form.name, {label: 'name', attr: {
placeholder: '/m/',
'data-controller': 'input-length',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\DTO\\MagazineDto::MAX_NAME_LENGTH')
}}) }}
{{ form_row(form.title, {label: 'title', attr: {
'data-controller': 'input-length autogrow',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\DTO\\MagazineDto::MAX_TITLE_LENGTH')
}}) }}
{{ component('editor_toolbar', {id: 'magazine_description'}) }}
{{ form_row(form.description, {label: false, attr: {
placeholder: 'description',
'data-controller': 'input-length rich-textarea autogrow',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\Entity\\Magazine::MAX_DESCRIPTION_LENGTH')
}}) }}
{{ form_row(form.isAdult, {label:'is_adult', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.isPostingRestrictedToMods, {label:'magazine_posting_restricted_to_mods',row_attr: {class: 'checkbox'}}) }}
{{ form_label(form.discoverable, 'discoverable') }}
{{ form_widget(form.discoverable) }}
{{ form_help(form.discoverable) }}
{{ form_label(form.indexable, 'indexable_by_search_engines') }}
{{ form_widget(form.indexable) }}
{{ form_help(form.indexable) }}
{{ form_label(form.nameAsTag, 'magazine_name_as_tag') }}
{{ form_widget(form.nameAsTag) }}
{{ form_help(form.nameAsTag) }}
{{ form_row(form.submit, {label: 'create_new_magazine', attr: {class: 'btn btn__primary'}, row_attr: {class: 'float-end'}}) }}
{{ form_end(form) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/list_abandoned.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'magazines'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazines page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/_options.html.twig' %}
{% if magazines|length %}
{{ 'name'|trans }}
{% for column in ['threads', 'comments', 'posts'] %}
{{ column|trans }}
{% endfor %}
{% for magazine in magazines %}
{{ component('magazine_inline', { magazine: magazine, stretchedLink: true, showAvatar: true, showNewIcon: true, fullName: true}) }}
{{ magazine.entryCount }}
{{ magazine.entryCommentCount }}
{{ magazine.postCount + magazine.postCommentCount }}
{% endfor %}
{% for magazine in magazines %}
{{ component('magazine_inline', { magazine: magazine, stretchedLink: true, showAvatar: true, showNewIcon: true, fullName: true}) }}
{{ magazine.entryCount }} {{ 'threads'|trans }}
{{ magazine.entryCommentCount }} {{ 'comments'|trans }}
{{ magazine.postCount + magazine.postCommentCount }} {{ 'posts'|trans }}
{% endfor %}
{% if(magazines.haveToPaginate is defined and magazines.haveToPaginate) %}
{{ pagerfanta(magazines, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/list_all.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'magazines'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazines page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/_options.html.twig' %}
{{ form_start(form) }}
{{ form_widget(form.query, {'attr': {'class': 'form-control'}}) }}
{{ form_widget(form.fields, {attr: {'aria-label': 'filter.fields.label'|trans}}) }}
{{ form_widget(form.federation, {attr: {'aria-label': 'filter.origin.label'|trans}}) }}
{{ form_widget(form.adult, {attr: {'aria-label': 'filter.adult.label'|trans}}) }}
{{ form_end(form) }}
{% include 'magazine/_list.html.twig' %}
{% endblock %}
================================================
FILE: templates/magazine/moderators.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderators'|trans }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-moderators{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'moderators'|trans }}
{% if app.user and app.user.visibility is same as 'visible' %}
{% if magazine.apId is same as null and not magazine.userIsModerator(app.user) %}
{% endif %}
{% if magazine.isAbandoned() and not magazine.userIsOwner(app.user) %}
{% endif %}
{% endif %}
{% if moderators|length %}
{% include 'magazine/_moderators_list.html.twig' %}
{% if(moderators.haveToPaginate is defined and moderators.haveToPaginate) %}
{{ pagerfanta(moderators, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/panel/_options.html.twig
================================================
{%- set STATUS_PENDING = constant('App\\Entity\\Report::STATUS_PENDING') -%}
================================================
FILE: templates/magazine/panel/_stats_pills.html.twig
================================================
{%- set TYPE_CONTENT = constant('App\\Repository\\StatsRepository::TYPE_CONTENT') -%}
{%- set TYPE_VOTES = constant('App\\Repository\\StatsRepository::TYPE_VOTES') -%}
================================================
FILE: templates/magazine/panel/badges.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'badges'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-badges{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'badges'|trans }}
{% if badges|length %}
{% for badge in badges %}
{{ badge.name }}
{% if is_granted('edit', magazine) %}
{% endif %}
{% endfor %}
{% endif %}
{% if(badges.haveToPaginate is defined and badges.haveToPaginate) %}
{{ pagerfanta(badges, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not badges|length %}
{% endif %}
{{ form_start(form) }}
{{ form_errors(form.name) }}
{{ form_label(form.name, 'name') }}
{{ form_widget(form.name) }}
{{ form_row(form.submit, { 'label': 'add_badge', attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/ban.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'bans'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-bans{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'ban'|trans }}
{{ component('user_box', {user: user}) }}
{{ form_start(form) }}
{{ form_label(form.reason, 'reason') }}
{{ form_widget(form.reason) }}
{{ form_label(form.expiredAt, 'expired_at') }}
{{ form_widget(form.expiredAt) }}
{{ form_row(form.submit, { 'label': 'ban', attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/bans.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'bans'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-bans{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'bans'|trans }}
{% if bans|length %}
{{ 'name'|trans }}
{{ 'reason'|trans }}
{{ 'created'|trans }}
{{ 'expires'|trans }}
{% for ban in bans %}
{{ component('user_inline', {user: ban.user, showNewIcon: true}) }}
{{ ban.reason }}
{{ component('date', {date: ban.createdAt}) }}
{% if ban.expiredAt %}
{{ component('date', {date: ban.expiredAt}) }}
{% else %}
{{ 'perm'|trans }}
{% endif %}
{% endfor %}
{% endif %}
{% if(bans.haveToPaginate is defined and bans.haveToPaginate) %}
{{ pagerfanta(bans, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not bans|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/panel/general.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'general'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-general{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'general'|trans }}
{% include 'layout/_flash.html.twig' %}
{{ form_start(form) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_label(form.title) }}
{{ form_widget(form.title) }}
{{ component('editor_toolbar', {id: 'magazine_description'}) }}
{{ form_row(form.description, {label: false, attr: {placeholder: 'description', 'data-entry-link-create-target': 'magazine_description'}}) }}
{% if form.rules is defined and form.rules %}
{{ component('editor_toolbar', {id: 'magazine_rules'}) }}
{{ form_row(form.rules, {label: false, attr: {placeholder: 'rules', 'data-entry-link-create-target': 'magazine_rules'}}) }}
{% endif %}
{{ form_label(form.isAdult) }}
{{ form_widget(form.isAdult) }}
{{ form_label(form.isPostingRestrictedToMods) }}
{{ form_widget(form.isPostingRestrictedToMods) }}
{{ form_label(form.discoverable, 'discoverable') }}
{{ form_widget(form.discoverable) }}
{{ form_help(form.discoverable) }}
{{ form_label(form.indexable, 'indexable_by_search_engines') }}
{{ form_widget(form.indexable) }}
{{ form_help(form.indexable) }}
{{ form_row(form.submit, { 'label': 'done'|trans, 'attr': {'class': 'btn btn__primary'} }) }}
{{ form_end(form) }}
{{ 'magazine_deletion'|trans }}
{% if magazine.visibility is same as 'visible' %}
{% else %}
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
{% endif %}
{% if is_granted('purge', magazine) %}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/panel/moderator_requests.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderators'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-moderators{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'moderators'|trans }}
{% if requests|length %}
{{ 'magazine'|trans }}
{{ 'user'|trans }}
{{ 'reputation_points'|trans }}
{% for request in requests %}
{{ component('magazine_inline', {magazine: request.magazine, showNewIcon: true}) }}
{{ component('user_inline', {user: request.user, showNewIcon: true}) }}
{{ get_reputation_total(request.user) }}
{% endfor %}
{% else %}
{% endif %}
{% if(requests.haveToPaginate is defined and requests.haveToPaginate) %}
{{ pagerfanta(requests, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/magazine/panel/moderators.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderators'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-moderators{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'moderators'|trans }}
{% include 'magazine/_moderators_list.html.twig' %}
{% if(moderators.haveToPaginate is defined and moderators.haveToPaginate) %}
{{ pagerfanta(moderators, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not moderators|length %}
{% endif %}
{{ form_start(form) }}
{{ form_errors(form.user) }}
{{ form_label(form.user, 'username') }}
{{ form_widget(form.user) }}
{{ form_row(form.submit, { 'label': 'add_moderator', attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/reports.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reports'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-reports{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'reports'|trans }}
{{ component('report_list', {reports: reports, routeName: 'magazine_panel_reports', magazineName: magazine.name}) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/stats.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'stats'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-stats{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{% include 'magazine/panel/_stats_pills.html.twig' %}
{% include 'stats/_filters.html.twig' %}
{{ render_chart(chart) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/tags.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'tags'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-tags{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'tags'|trans }}
{{ 'magazine_panel_tags_info'|trans }}
{{ form_start(form) }}
{{ form_row(form.submit, { 'label': 'save'|trans, attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/theme.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'appearance'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-theme{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'appearance'|trans }}
{% include 'layout/_flash.html.twig' %}
{{ form_start(form) }}
{{ form_label(form.icon, 'icon') }}
{{ form_widget(form.icon) }}
{{ form_help(form.icon) }}
{{ form_errors(form.icon) }}
{% if magazine.icon is not same as null %}
{% endif %}
{{ form_label(form.banner, 'banner') }}
{{ form_widget(form.banner) }}
{{ form_help(form.banner) }}
{{ form_errors(form.banner) }}
{% if magazine.banner is not same as null %}
{% endif %}
{{ form_label(form.customCss, 'CSS') }}
{{ form_widget(form.customCss) }}
{{ form_help(form.customCss) }}
{{ form_errors(form.customCss) }}
{{ form_label(form.backgroundImage, 'Background') }}
{{ form_widget(form.backgroundImage) }}
{{ form_help(form.backgroundImage) }}
{{ form_errors(form.backgroundImage) }}
{{ form_row(form.submit, { 'label': 'done', attr: {class: 'btn btn__primary'} }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/magazine/panel/trash.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'trash'|trans }} - {{ 'magazine_panel'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-magazine-panel page-magazine-trash{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'magazine/panel/_options.html.twig' %}
{% include 'magazine/_visibility_info.html.twig' %}
{{ 'trash'|trans }}
{% if results|length %}
{% for subject in results %}
{% include 'layout/_subject.html.twig' with {attributes: {canSeeTrash: true, showMagazineName: true, showEntryTitle: true}} %}
{% endfor %}
{% endif %}
{% if(results.haveToPaginate is defined and results.haveToPaginate) %}
{{ pagerfanta(results, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not results|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/messages/_form_create.html.twig
================================================
{{ form_start(form, {attr: {class: 'message-form'}}) }}
{{ field_name(form.body) }}
{{ form_row(form.submit, {attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/messages/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'messages'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-messages{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'messages'|trans }}
{% for thread in threads %}
{% set lastMessage = thread.getLastMessage() %}
{% if lastMessage is not same as null %}
{% 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, showNewIcon: true}) }}
{% set i = i + 1 %}
{% endfor %}
{{ component('date', {date: thread.updatedAt}) }}
{% endif %}
{% endfor %}
{% if threads|length == 0 %}
{% endif %}
{% if(threads.haveToPaginate is defined and threads.haveToPaginate) %}
{{ pagerfanta(threads, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/messages/single.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'message'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-messages page-message{% endblock %}
{% block header_nav %}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% 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, showNewIcon: true}) }}
{% set i = i + 1 %}
{% endfor %}
{% for message in thread.messages %}
{{ component('user_inline', {user: message.sender, showNewIcon: true}) }}
{{ message.body|markdown|raw }}
{{ component('date', {date: message.createdAt}) }}
{% if message.editedAt %}
({{ 'edited'|trans }} {{ component('date', {date: message.editedAt}) }})
{% endif %}
{% endfor %}
{% include 'messages/_form_create.html.twig' %}
{% endblock %}
================================================
FILE: templates/modlog/_blocks.html.twig
================================================
{% block log_entry_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_thread_by'|trans|lower }} {{ component('user_inline', {user: log.entry.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.entry.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.entry.shortTitle(300) }}
{% endblock %}
{% block log_entry_restored %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'restored_thread_by'|trans|lower }} {{ component('user_inline', {user: log.entry.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.entry.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.entry.shortTitle(300) }}
{% endblock %}
{% block log_entry_comment_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
{% endblock %}
{% block log_entry_comment_restored %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'restored_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
{% endblock %}
{% block log_post_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_post_by'|trans|lower }} {{ component('user_inline', {user: log.post.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.post.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.post.shortTitle(300) }}
{% endblock %}
{% block log_post_restored %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'restored_post_by'|trans|lower }} {{ component('user_inline', {user: log.post.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.post.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.post.shortTitle(300) }}
{% endblock %}
{% block log_post_comment_deleted %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'removed_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
{% endblock %}
{% block log_post_comment_restored %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }} {{ 'restored_comment_by'|trans|lower }} {{ component('user_inline', {user: log.comment.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.comment.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %} -
{{ log.comment.shortTitle(300) }}
{% endblock %}
{% block log_ban %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% if log.meta is same as 'ban' %}
{{ 'he_banned'|trans|lower }}
{% else %}
{{ 'he_unbanned'|trans|lower }}
{% endif %}
{{ component('user_inline', {user: log.ban.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.ban.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) }}{% endif %}{% if log.ban.reason %} - {{ log.ban.reason }}{% endif %}
{% endblock %}
{% block log_moderator_add %}
{% if log.actingUser is not same as null %}
{{ component('user_inline', {user: log.actingUser, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% else %}
{{ 'someone'|trans }}
{% endif %}
{{ 'magazine_log_mod_added'|trans -}}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}: {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% endblock %}
{% block log_moderator_remove %}
{% if log.actingUser is not same as null %}
{{ component('user_inline', {user: log.actingUser, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% else %}
{{ 'someone'|trans }}
{% endif %}
{{ 'magazine_log_mod_removed'|trans -}}
{% if showMagazine %} {{ 'from'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}: {{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% endblock %}
{% block log_entry_pinned %}
{% if log.actingUser is not same as null %}
{{ component('user_inline', {user: log.actingUser, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% else %}
{{ 'someone'|trans }}
{% endif %}
{{ 'magazine_log_entry_pinned'|trans }}
{{ log.entry.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
{% block log_entry_unpinned %}
{% if log.actingUser is not same as null %}
{{ component('user_inline', {user: log.actingUser, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{% else %}
{{ 'someone'|trans }}
{% endif %}
{{ 'magazine_log_entry_unpinned'|trans }}
{{ log.entry.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
{% block log_entry_locked %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{{ 'magazine_log_entry_locked'|trans }}
{{ log.entry.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
{% block log_entry_unlocked %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{{ 'magazine_log_entry_unlocked'|trans }}
{{ log.entry.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
{% block log_post_locked %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{{ 'magazine_log_entry_locked'|trans }}
{{ log.post.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
{% block log_post_unlocked %}
{{ component('user_inline', {user: log.user, showAvatar: showAvatars, showNewIcon: showNewIcons}) }}
{{ 'magazine_log_entry_unlocked'|trans }}
{{ log.post.shortTitle(300) }}
{% if showMagazine %} {{ 'in'|trans|lower }} {{ component('magazine_inline', {magazine: log.magazine, showAvatar: showIcons, showNewIcon: showNewIcons}) -}}{%- endif -%}
{% endblock %}
================================================
FILE: templates/modlog/front.html.twig
================================================
{% extends 'base.html.twig' %}
{% set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') %}
{% set MBIN_MODERATION_LOG_SHOW_USER_AVATARS = constant('App\\Controller\\User\\ThemeSettingsController::MBIN_MODERATION_LOG_SHOW_USER_AVATARS') %}
{% set showAvatars = app.request.cookies.get(MBIN_MODERATION_LOG_SHOW_USER_AVATARS) is same as V_TRUE %}
{% set MBIN_MODERATION_LOG_SHOW_MAGAZINE_ICONS = constant('App\\Controller\\User\\ThemeSettingsController::MBIN_MODERATION_LOG_SHOW_MAGAZINE_ICONS') %}
{% set showIcons = app.request.cookies.get(MBIN_MODERATION_LOG_SHOW_MAGAZINE_ICONS) is same as V_TRUE %}
{% set MBIN_MODERATION_LOG_SHOW_NEW_ICONS = constant('App\\Controller\\User\\ThemeSettingsController::MBIN_MODERATION_LOG_SHOW_NEW_ICONS') %}
{% set showNewIcons = app.request.cookies.get(MBIN_MODERATION_LOG_SHOW_NEW_ICONS, V_TRUE) is same as V_TRUE %}
{% use 'modlog/_blocks.html.twig' %}
{% set hasMagazine = magazine is defined and magazine ? true : false %}
{%- block title -%}
{% if hasMagazine %}
{{- 'mod_log'|trans }} - {{ magazine.title }} - {{ parent() -}}
{% else %}
{{- 'mod_log'|trans }} - {{ parent() -}}
{% endif %}
{%- endblock -%}
{% block mainClass %}page-modlog{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'mod_log'|trans }}
{{ 'mod_log_alert'|trans }}
{{ form_start(form) }}
{{ form_widget(form.magazine, {'attr': {'onchange': '(function (e){ e.target.form.submit();})(event)'}}) }}
{{ form_widget(form.types) }}
{{ form_end(form) }}
{% for log in logs %}
{%- with {
log: log,
showMagazine: not hasMagazine,
showAvatars: showAvatars,
showIcons: showIcons,
showNewIcons: showNewIcons,
} only -%}
{{ block(log.type) }}
{%- endwith -%}
{{ component('date', {date: log.createdAt}) }}
{% endfor %}
{% if(logs.haveToPaginate is defined and logs.haveToPaginate) %}
{{ pagerfanta(logs, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not logs|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/notifications/_blocks.html.twig
================================================
{% block entry_created_notification %}
{{ component('user_inline', {user: notification.entry.user, showNewIcon: true}) }} {{ 'added_new_thread'|trans|lower }} - {{ notification.entry.shortTitle }}
{% endblock entry_created_notification %}
{% block entry_edited_notification %}
{{ component('user_inline', {user: notification.entry.user, showNewIcon: true}) }} {{ 'edited_thread'|trans|lower }} - {{ notification.entry.shortTitle }}
{% endblock entry_edited_notification %}
{% block entry_deleted_notification %}
{{ notification.entry.shortTitle }}
{% if notification.entry.isTrashed %}{{ 'removed'|trans|lower }}{% else %}{{ 'deleted'|trans|lower }}{% endif %}
{% endblock entry_deleted_notification %}
{% block entry_mentioned_notification %}
{{ component('user_inline', {user: notification.entry.user, showNewIcon: true}) }} {{ 'mentioned_you'|trans|lower }} - {{ notification.entry.shortTitle }}
{% endblock entry_mentioned_notification %}
{% block entry_comment_created_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'added_new_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock entry_comment_created_notification %}
{% block entry_comment_edited_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'edited_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock entry_comment_edited_notification %}
{% block entry_comment_reply_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'replied_to_your_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock entry_comment_reply_notification %}
{% block entry_comment_deleted_notification %}
{{ 'comment'|trans }} {{ notification.comment.shortTitle }} -
{% if notification.comment.isTrashed %}{{ 'removed'|trans|lower }}{% else %}{{ 'deleted'|trans|lower }}{% endif %}
{% endblock entry_comment_deleted_notification %}
{% block entry_comment_mentioned_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'mentioned_you'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock entry_comment_mentioned_notification %}
{% block post_created_notification %}
{{ component('user_inline', {user: notification.post.user, showNewIcon: true}) }} {{ 'added_new_post'|trans|lower }} - {{ notification.post.shortTitle }}
{% endblock post_created_notification %}
{% block post_edited_notification %}
{{ component('user_inline', {user: notification.post.user, showNewIcon: true}) }} {{ 'edit_post'|trans|lower }} - {{ notification.post.shortTitle }}
{% endblock post_edited_notification %}
{% block post_deleted_notification %}
{{ 'post'|trans }} {{ notification.post.shortTitle }} -
{% if notification.comment.isTrashed %}{{ 'removed'|trans|lower }}{% else %}{{ 'deleted'|trans|lower }}{% endif %}
{% endblock post_deleted_notification %}
{% block post_mentioned_notification %}
{{ component('user_inline', {user: notification.post.user, showNewIcon: true}) }} {{ 'mentioned_you'|trans|lower }} - {{ notification.post.shortTitle }}
{% endblock post_mentioned_notification %}
{% block post_comment_created_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'added_new_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock post_comment_created_notification %}
{% block post_comment_edited_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'edited_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock post_comment_edited_notification %}
{% block post_comment_reply_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'replied_to_your_comment'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock post_comment_reply_notification %}
{% block post_comment_deleted_notification %}
{{ 'comment'|trans }} {{ notification.comment.shortTitle }} -
{% if notification.comment.isTrashed %}{{ 'removed'|trans|lower }}{% else %}{{ 'deleted'|trans|lower }}{% endif %}
{% endblock post_comment_deleted_notification %}
{% block post_comment_mentioned_notification %}
{{ component('user_inline', {user: notification.comment.user, showNewIcon: true}) }} {{ 'mentioned_you'|trans|lower }} - {{ notification.comment.shortTitle }}
{% endblock post_comment_mentioned_notification %}
{% block message_notification %}
{{ component('user_inline', {user: notification.message.sender, showNewIcon: true}) }} {{ 'wrote_message'|trans|lower }} {{ notification.message.title }}
{% endblock message_notification %}
{% block magazine_ban_notification %}
{% if notification.ban.expiredAt is not same as null -%}
{{ 'you_have_been_banned_from_magazine'|trans({'%m': component('magazine_inline', {'magazine': notification.ban.magazine})})|raw }}
{% if now() < notification.ban.expiredAt %}
{{ 'ban_expires'|trans }}:
{% else %}
{{ 'ban_expired'|trans }}:
{% endif %}
{{ component('date', {date: notification.ban.expiredAt}) -}}.
{% else -%}
{{ 'you_have_been_banned_from_magazine_permanently'|trans({'%m': component('magazine_inline', {'magazine': notification.ban.magazine})})|raw }}
{% endif -%}
{{ 'reason'|trans }}: {{ notification.ban.reason }}
{% endblock magazine_ban_notification %}
{% block magazine_unban_notification %}
{{ 'you_are_no_longer_banned_from_magazine'|trans({'%m': component('magazine_inline', {'magazine': notification.ban.magazine})})|raw }}
{% endblock magazine_unban_notification %}
{% block reportlink %}
{% if notification.report.entry is defined and notification.report.entry is not same as null %}
{% set entry = notification.report.entry %}
{{ entry.title }}
{% elseif notification.report.entryComment is defined and notification.report.entryComment is not same as null %}
{% set entryComment = notification.report.entryComment %}
{{ entryComment.getShortTitle() }}
{% elseif notification.report.post is defined and notification.report.post is not same as null %}
{% set post = notification.report.post %}
{{ post.getShortTitle() }}
{% elseif notification.report.postComment is defined and notification.report.postComment is not same as null %}
{% set postComment = notification.report.postComment %}
{{ postComment.getShortTitle() }}
{% endif %}
{% endblock %}
{% block report_created_notification %}
{{ component('user_inline', {user: notification.report.reporting, showNewIcon: true}) }} {{ 'reported'|trans|lower }} {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
{% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
{{ 'open_report'|trans }}
{% endif %}
{% endblock report_created_notification %}
{% block report_rejected_notification %}
{{ 'own_report_rejected'|trans }}
{{ 'reported_user'|trans }}: {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
{% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
{{ 'open_report'|trans }}
{% endif %}
{% endblock report_rejected_notification %}
{% block report_approved_notification %}
{% if notification.report.reporting.id is same as app.user.id %}
{{ 'own_report_accepted'|trans }}
{% elseif notification.report.reported.id is same as app.user.id %}
{{ 'own_content_reported_accepted'|trans }}
{% else %}
{{ 'report_accepted'|trans }}
{{ 'reported_user'|trans }}: {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{{ 'reporting_user'|trans }}: {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{% endif %}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
{% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
{{ 'open_report'|trans }}
{% endif %}
{% endblock report_approved_notification %}
{% block new_signup %}
{{ 'notification_title_new_signup'|trans }}
{{ component('user_inline', { user: notification.newUser, showNewIcon: true } ) }}
{% if do_new_users_need_approval() and notification.newUser.applicationStatus is not same as enum('App\\Enums\\EApplicationStatus').Approved %}
{% endif %}
{% endblock %}
================================================
FILE: templates/notifications/front.html.twig
================================================
{% extends 'base.html.twig' %}
{% use 'notifications/_blocks.html.twig' %}
{%- block title -%}
{{- 'notifications'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-notifications{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'notifications'|trans }}
{{ 'test_push_notifications_button'|trans }}
{{ 'register_push_notifications_button'|trans }}
{{ 'unregister_push_notifications_button'|trans }}
{% for notification in notifications %}
{%- with {
notification: notification,
showMagazine: false,
} only -%}
{{ block(notification.type) }}
{%- endwith -%}
{{ component('date', {date: notification.createdAt}) }}
{% endfor %}
{% if(notifications.haveToPaginate is defined and notifications.haveToPaginate) %}
{{ pagerfanta(notifications, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not notifications|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/page/about.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'about_instance'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-about{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'about_instance'|trans }}
{{ body|markdown|raw }}
{% endblock %}
================================================
FILE: templates/page/agent.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'kbin_bot'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-bot{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'kbin_bot'|trans }}
{{- 'bot_body_content'|trans|nl2br }}
{% endblock %}
================================================
FILE: templates/page/contact.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'contact'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-contact page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'contact'|trans }}
{% include 'layout/_flash.html.twig' %}
{{ form_start(form) }}
{{ form_row(form.name, {label: 'firstname'}) }}
{{ form_row(form.email, {label: 'email'}) }}
{{ form_row(form.message, {label: 'message'}) }}
{{ form_row(form.surname, {label: false, attr: {style: 'display:none !important'}}) }}
{% if kbin_captcha_enabled() %}
{{ form_row(form.captcha, {
label: false
}) }}
{% endif %}
{{ form_row(form.submit, {label: 'send', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% if body %}
{{ body|markdown|raw }}
{% endif %}
{% endblock %}
================================================
FILE: templates/page/faq.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'faq'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-faq{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'faq'|trans }}
{{ body|markdown|raw }}
{% endblock %}
================================================
FILE: templates/page/federation.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'federation'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-federation{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'federation'|trans }}
{{'federation_page_allowed_description'|trans}}
{% if allowedInstances is not empty %}
{{ component('instance_list', {'instances': allowedInstances}) }}
{% else %}
{% endif %}
{{'federation_page_disallowed_description'|trans}}
{% if defederatedInstances is not empty %}
{{ component('instance_list', {'instances': defederatedInstances}) }}
{% else %}
{% endif %}
{{'federation_page_dead_title'|trans}}
{{ 'federation_page_dead_description'|trans }}
{% if deadInstances is not empty %}
{{ component('instance_list', {'instances': deadInstances}) }}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/page/privacy_policy.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'privacy_policy'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-privacy-policy{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'privacy_policy'|trans }}
{{ body|markdown|raw }}
{% endblock %}
================================================
FILE: templates/page/terms.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'terms'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-terms{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'terms'|trans }}
{{ body|markdown|raw }}
{% endblock %}
================================================
FILE: templates/people/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{% if magazine is defined and magazine %}
{{- 'people'|trans }} - {{ magazine.title }} - {{ parent() -}}
{% else %}
{{- 'people'|trans }} - {{ parent() -}}
{% endif %}
{%- endblock -%}
{% block mainClass %}page-people{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'people'|trans }}
{{ 'people_local'|trans }}
{% for user in local %}
{{ component('user_box', {user: user}) }}
{% endfor %}
{% if not local|length %}
{% endif %}
{{ 'people_federated'|trans }}
{% for user in federated %}
{{ component('user_box', {user: user}) }}
{% endfor %}
{% if not federated|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/post/_form_post.html.twig
================================================
{% form_theme form.lang 'form/lang_select.html.twig' %}
{% set hasImage = false %}
{% if post is defined and post is not null and post.image %}
{% set hasImage = true %}
{% endif %}
{% if edit is not defined %}
{% set edit = false %}
{% endif %}
{% if edit %}
{% set title = 'edit_post'|trans %}
{% set action = path('post_edit', {magazine_name: post.magazine.name, post_id: post.id}) %}
{% else %}
{% set title = 'add_post'|trans %}
{% set action = path('post_create') %}
{% endif %}
{% if attributes is not defined %}
{% set attributes = {} %}
{% endif %}
{{ title }}
{{ form_start(form, {action: action, attr: {class: edit ? 'post-edit replace' : 'post-add'}|merge(attributes)}) }}
{{ component('editor_toolbar', {id: 'post_body'}) }}
{{ form_row(form.body, {label: false, attr: {
'data-controller': 'input-length rich-textarea autogrow',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\DTO\\PostDto::MAX_BODY_LENGTH')
}}) }}
{{ form_row(form.isAdult, {label:'is_adult'}) }}
{{ form_row(form.magazine, {label: false, attr: {placeholder: false}}) }}
{% if hasImage %}
{% endif %}
{{ form_row(form.lang, {label: false}) }}
{{ form_row(form.submit, {label: edit ? 'edit_post' : 'add_post', attr: {class: 'btn btn__primary', 'data-action': 'subject#sendForm'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/post/_info.html.twig
================================================
{{ 'thread'|trans }}
{% if post.user.avatar %}
{% endif %}
{{ post.user.username|username(true) }}
{% if post.user.apManuallyApprovesFollowers is same as true %}
{% endif %}
{% if post.user.apProfileId %}
{% endif %}
{{ component('user_actions', {user: post.user}) }}
{% if app.user is defined and app.user is not same as null and app.user is not same as post.user %}
{{ component('notification_switch', {target: post.user}) }}
{% endif %}
{{ 'added'|trans }}: {{ component('date', {date: post.createdAt}) }}
{{ 'up_votes'|trans }}:
{{ post.countUpvotes }}
================================================
FILE: templates/post/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_COMMENT_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR'), V_TRUE) -%}
{%- set SHOW_POST_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_USERS_AVATARS'), V_TRUE) -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set INFINITE_SCROLL = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_INFINITE_SCROLL'), V_FALSE) -%}
================================================
FILE: templates/post/_menu.html.twig
================================================
{{ 'more'|trans }}
================================================
FILE: templates/post/_moderate_panel.html.twig
================================================
{% if is_granted('purge', post) %}
{% endif %}
{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% endif %}
{{ form_start(form, {action: path('post_change_lang', {magazine_name: magazine.name, post_id: post.id})}) }}
{{ form_row(form.lang, {label: false, row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.submit, {label: 'change_language'|trans, attr: {class: 'btn btn__secondary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/post/_options.html.twig
================================================
{% set showFilterLabels = app.request.cookies.get('kbin_general_filter_labels')|default('on') %}
{{ criteria.getOption('sort')|trans }}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('time') != 'all') %}
{{ criteria.getOption('time')|trans }}
{% endif %}
{% if criteria and criteria.getOption('content') != 'microblog' %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('type') != 'all') %}
{{ criteria.getOption('type')|trans }}
{% endif %}
{% endif %}
{% if app.user %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and (criteria.favourite or criteria.moderated or criteria.subscribed)) %}
{{ criteria.resolveSubscriptionFilter()|trans }}
{% endif %}
{% endif %}
{% if showFilterLabels == 'on' or (showFilterLabels == 'auto' and criteria.getOption('federation') != 'all') %}
{{ criteria.federation|trans }}
{% endif %}
================================================
FILE: templates/post/_options_activity.html.twig
================================================
================================================
FILE: templates/post/comment/_form_comment.html.twig
================================================
{% form_theme form.lang 'form/lang_select.html.twig' %}
{% set hasImage = false %}
{% if comment is defined and comment is not null and comment.image %}
{% set hasImage = true %}
{% endif %}
{% if edit is not defined %}
{% set edit = false %}
{% endif %}
{% if edit %}
{% set title = 'edit_comment'|trans %}
{% set action = path('post_comment_edit', {magazine_name: post.magazine.name, post_id: post.id, comment_id: comment.id}) %}
{% else %}
{% set title = 'add_comment'|trans %}
{% set action = path('post_comment_create', {magazine_name: post.magazine.name, post_id: post.id, parent_comment_id: parent is defined and parent ? parent.id : null}) %}
{% endif %}
{{ title }}
{{ form_start(form, {action: action, attr: {class: edit ? 'comment-edit replace' : 'comment-add'}}) }}
{{ component('editor_toolbar', {id: form.body.vars.id}) }}
{{ form_row(form.body, {label: false, attr: {
'data-controller': 'input-length rich-textarea autogrow',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value': constant('App\\DTO\\PostCommentDto::MAX_BODY_LENGTH')
}}) }}
{% if hasImage %}
{% endif %}
{{ form_row(form.lang, {label: false}) }}
{{ form_row(form.submit, {label: edit ? 'edit_comment' : 'add_comment', attr: {class: 'btn btn__primary', 'data-action': 'subject#sendForm'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/post/comment/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set V_CHAT = constant('App\\Controller\\User\\ThemeSettingsController::CHAT') -%}
{%- set V_TREE = constant('App\\Controller\\User\\ThemeSettingsController::TREE') -%}
{%- set SHOW_COMMENT_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR'), V_TRUE) -%}
{%- set SHOW_POST_USER_AVATARS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_USERS_AVATARS'), V_TRUE) -%}
{%- set DYNAMIC_LISTS = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_GENERAL_DYNAMIC_LISTS'), V_FALSE) -%}
{%- set VIEW_STYLE = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::POST_COMMENTS_VIEW'), V_TREE) -%}
{% if showNested is not defined %}
{% if VIEW_STYLE is same as V_CHAT %}
{% set showNested = false %}
{% else %}
{% set showNested = true %}
{% endif %}
{% endif %}
{% if level is not defined %}
{% set level = 1 %}
{% endif %}
{% set autoAction = is_route_name('post_single') ? 'notifications:PostCommentCreatedNotification@window->subject-list#addComment' : 'notifications:PostCommentCreatedNotification@window->subject-list#addCommentOverview' %}
{% set manualAction = is_route_name('post_single') ? 'notifications:PostCommentCreatedNotification@scroll-top#increaseCounter' : 'notifications:PostCommentCreatedNotification@window->scroll_top#increaseCounter' %}
================================================
FILE: templates/post/comment/_menu.html.twig
================================================
{{ 'more'|trans }}
================================================
FILE: templates/post/comment/_moderate_panel.html.twig
================================================
{% if is_granted('purge', comment) %}
{% endif %}
{{ form_start(form, {action: path('post_comment_change_lang', {magazine_name: magazine.name, post_id: post.id, comment_id: comment.id})}) }}
{{ form_row(form.lang, {label: false, row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.submit, {label: 'change_language'|trans, attr: {class: 'btn btn__secondary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/post/comment/_no_comments.html.twig
================================================
================================================
FILE: templates/post/comment/_options.html.twig
================================================
================================================
FILE: templates/post/comment/_options_activity.html.twig
================================================
================================================
FILE: templates/post/comment/_preview.html.twig
================================================
================================================
FILE: templates/post/comment/create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'add_comment'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-comment-create{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post', {
post: post,
isSingle: true,
showMagazineName: false
}) }}
{% if parent is defined and parent %}
{{ component('post_comment', {
comment: parent,
showEntryTitle: false,
showNested: false
}) }}
{% endif %}
{% include 'layout/_flash.html.twig' %}
{% if user.visibility is same as 'visible'%}
{% include 'post/comment/_form_comment.html.twig' %}
{% endif %}
{% endblock %}
================================================
FILE: templates/post/comment/edit.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'edit_comment'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-comment-edit{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post_comment', {
comment: comment,
dateAsUrl: false,
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/comment/_form_comment.html.twig' with {edit: true} %}
{% endblock %}
================================================
FILE: templates/post/comment/favourites.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'favourites'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-comment-favourites{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post_comment', {
comment: comment
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/comment/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: favourites} %}
{% endblock %}
================================================
FILE: templates/post/comment/moderate.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderate'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-moderate{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post_comment', {
comment: comment,
isSingle: true,
dateAsUrl: false,
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/comment/_moderate_panel.html.twig' %}
{% endblock %}
================================================
FILE: templates/post/comment/voters.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'activity'|trans }} - {{ get_short_sentence(comment.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-comment-voters{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post_comment', {
comment: comment
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/comment/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: votes} %}
{% endblock %}
================================================
FILE: templates/post/create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'add_new_post'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-create{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% if magazine is defined and magazine %}
{{ magazine.title }}
{{ get_active_sort_option()|trans }}
{% else %}
{{ get_active_sort_option()|trans }}
{% endif %}
{% include 'layout/_flash.html.twig' %}
{% include 'post/_form_post.html.twig' %}
{% endblock %}
================================================
FILE: templates/post/edit.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'edit'|trans }} - {{ get_short_sentence(post.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-edit{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% if magazine is defined and magazine %}
{{ magazine.title }}
{{ get_active_sort_option()|trans }}
{% else %}
{{ get_active_sort_option()|trans }}
{% endif %}
{{ component('post', {
post: post,
isSingle: true,
dateAsUrl: false,
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/_form_post.html.twig' with {edit: true} %}
{% endblock %}
================================================
FILE: templates/post/favourites.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'favourites'|trans }} - {{ get_short_sentence(post.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-favourites{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post', {
post: post,
isSingle: true
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: favourites} %}
{% endblock %}
================================================
FILE: templates/post/moderate.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderate'|trans }} - {{ get_short_sentence(post.body, 80) }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-moderate{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post', {
post: post,
isSingle: true,
dateAsUrl: false,
class: 'section--top',
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/_moderate_panel.html.twig' %}
{% endblock %}
================================================
FILE: templates/post/single.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- get_short_sentence(post.body, 80) }} - {{ magazine.title }} - {{ parent() -}}
{%- endblock -%}
{% block description %}{% endblock %}
{% block image %}
{%- if post.image -%}
{{- uploaded_asset(post.image) -}}
{%- elseif post.magazine.icon -%}
{{- uploaded_asset(post.magazine.icon) -}}
{%- else -%}
{{- parent() -}}
{%- endif -%}
{% endblock %}
{% block mainClass %}page-post-single{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% endblock %}
================================================
FILE: templates/post/voters.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'up_votes'|trans }} - {{ get_short_sentence(post.body, 80) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-post-voters{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('post', {
post: post,
isSingle: true
}) }}
{% include 'layout/_flash.html.twig' %}
{% include 'post/_options_activity.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: votes} %}
{% endblock %}
================================================
FILE: templates/report/_form_report.html.twig
================================================
{{ 'report'|trans }}
{% for flash in app.flashes('error') %}
{{ flash }}
{% endfor %}
{% for flash in app.flashes('info') %}
{{ flash }}
{% endfor %}
{{ form_start(form) }}
{{ form_row(form.reason, {label: 'reason'}) }}
{{ form_row(form.submit, {label: 'report', attr: {class: 'btn btn__primary', 'data-action': 'subject#sendForm'}, row_attr: {class: 'float-end'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/report/create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'report'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-report{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'layout/_subject.html.twig' with {entryAttributes: {class: 'section--top'}, postAttributes: {class: 'post--single section--top'}} %}
{% include 'report/_form_report.html.twig' %}
{% endblock %}
================================================
FILE: templates/resend_verification_email/resend.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'resend_account_activation_email'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-resend-activation-email{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'resend_account_activation_email'|trans }}
{{ 'resend_account_activation_email_description'|trans }}
{{ form_start(form) }}
{% for flash_error in app.flashes('error') %}
{{ flash_error|trans }}
{% endfor %}
{% for flash_success in app.flashes('success') %}
{{ flash_success|trans }}
{% endfor %}
{{ form_row(form.email) }}
{{ form_row(form.submit, {
row_attr: {
class: 'button-flex-hf'
}
}) }}
{{ form_end(form) }}
{{ component('user_form_actions', {showRegister: true, showPasswordReset: true, showResendEmail: true}) }}
{% endblock %}
================================================
FILE: templates/reset_password/check_email.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reset_password'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-reset-password-email-sent{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'check_email'|trans }}
{{ 'reset_check_email_desc'|trans({'%expire%': resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle')}) }}
{{ 'reset_check_email_desc2'|trans }}
{{ 'try_again'|trans }}
{% endblock %}
================================================
FILE: templates/reset_password/request.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reset_password'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-reset-password{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'reset_password'|trans }}
{{ form_start(form) }}
{% for flash_error in app.flashes() %}
{{ flash_error }}
{% endfor %}
{{ form_row(form.email, {
label: 'email'
}) }}
{{ 'reset_password'|trans }}
{{ form_end(form) }}
{{ component('user_form_actions', {showLogin: true, showRegister: true, showResendEmail: true}) }}
{% endblock %}
================================================
FILE: templates/reset_password/reset.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reset_password'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-reset-password{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'reset_password'|trans }}
{{ form_start(form) }}
{% for flash_error in app.flashes() %}
{{ flash_error }}
{% endfor %}
{{ form_row(form.plainPassword) }}
{{ 'reset_password'|trans }}
{{ form_end(form) }}
{{ component('user_form_actions', {showLogin: true, showRegister: true , showResendEmail: true}) }}
{% endblock %}
================================================
FILE: templates/search/_emoji_suggestion.html.twig
================================================
{% for emoji in emojis %}
{{ emoji.shortCode }}
{{ emoji.emoji }}
{% endfor %}
================================================
FILE: templates/search/_list.html.twig
================================================
{% include 'layout/_subject_list.html.twig' with {
entryCommentAttributes: {showMagazineName: true, showEntryTitle: true},
postCommentAttributes: {withPost: false},
magazineAttributes: {showMeta: false, showRules: false, showDescription: false, showInfo: false, stretchedLink: false, showTags: false},
userAttributes: {},
} %}
================================================
FILE: templates/search/_user_suggestion.html.twig
================================================
{% for user in users %}
{{ user.username|username(true) }}
{% endfor %}
================================================
FILE: templates/search/form.html.twig
================================================
{{ form_start(form, {'attr': {'class': 'search-form'}}) }}
{{ form_widget(form.q, {label: false, 'attr': {'class': 'form-control'}}) }}
{{ form_widget(form.magazine, {label: false, 'attr': {'class': 'form-control'}}) }}
{{ form_widget(form.user, {label: false, 'attr': {'class': 'form-control'}}) }}
{{ form_widget(form.type, {label: false, 'attr': {'class': 'form-control', 'style': 'padding: 1rem .5rem;'}}) }}
{{ form_label(form.since, 'created_since') }}
{{ form_widget(form.since, {'attr': {'class': 'form-control', 'style': 'padding: 1rem .5rem;', 'title': 'created_since'|trans}}) }}
{{ form_end(form) }}
================================================
FILE: templates/search/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'search'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-search page-settings{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'search'|trans }}
{% include 'search/form.html.twig' %}
{% include 'layout/_flash.html.twig' %}
{% endblock %}
================================================
FILE: templates/stats/_filters.html.twig
================================================
{{ 'all'|trans }}
{{ 'week'|trans }}
2 {{ 'weeks'|trans }}
{{ 'month'|trans }}
6 {{ 'months'|trans }}
{{ 'year'|trans }}
{{ 'local'|trans }}
{{ 'federated'|trans }}
================================================
FILE: templates/stats/_options.html.twig
================================================
{%- set TYPE_GENERAL = constant('App\\Repository\\StatsRepository::TYPE_GENERAL') -%}
{%- set TYPE_CONTENT = constant('App\\Repository\\StatsRepository::TYPE_CONTENT') -%}
{%- set TYPE_VOTES = constant('App\\Repository\\StatsRepository::TYPE_VOTES') -%}
================================================
FILE: templates/stats/_stats_count.html.twig
================================================
{% include 'stats/_filters.html.twig' %}
{{ 'users'|trans|upper }}
{{ users|abbreviateNumber }}
{{ 'magazines'|trans|upper }}
{{ magazines|abbreviateNumber }}
{{ 'votes'|trans|upper }}
{{ votes|abbreviateNumber }}
{{ 'threads'|trans|upper }}
{{ entries|abbreviateNumber }}
{{ 'comments'|trans|upper }}
{{ comments|abbreviateNumber }}
{{ 'posts'|trans|upper }}
{{ posts|abbreviateNumber }}
================================================
FILE: templates/stats/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'stats'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-stats{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'stats/_options.html.twig' %}
{{ 'stats'|trans }}
{% if route_has_param('statsType', 'general'|trans|lower) or chart is null %}
{% include 'stats/_stats_count.html.twig' %}
{% else %}
{% include 'stats/_filters.html.twig' %}
{{ render_chart(chart) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/styles/custom.css.twig
================================================
{#
using |raw here should be somewhat safe
since the next thing you fight should be the browser's css parser
#}
/* site dynamic styles */
{{ include('components/_details_label.css.twig') }}
{% if not app.user or not app.user.ignoreMagazinesCustomCss %}
{% if magazine is defined and magazine and magazine.customCss %}
/* magazine styles */
{{ magazine.customCss|raw }}
{% endif %}
{% endif %}
{% if app.user is defined and app.user and app.user.customCss %}
/* user styles */
{{ app.user.customCss|raw }}
{% endif %}
================================================
FILE: templates/tag/_list.html.twig
================================================
{% include 'layout/_subject_list.html.twig' with {entryCommentAttributes: {showMagazineName: true, showEntryTitle: true}, postCommentAttributes: {withPost: false}} %}
================================================
FILE: templates/tag/_options.html.twig
================================================
================================================
FILE: templates/tag/_panel.html.twig
================================================
{{ 'tag'|trans }}
{{ component('tag_actions', {tag: tag}) }}
{% if false %}
{{ component('magazine_sub', {magazine: magazine}) }}
{% if showInfo %}
{{ 'subscribers'|trans }}: {{ computed.magazine.subscriptionsCount }}
{% endif %}
{% endif %}
{% macro meta_item(name, count) %}
{{ name }}{{ count }}
{% endmacro %}
================================================
FILE: templates/tag/comments.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
#{{ tag }} - {{ 'comments'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-tag-comments{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'tag/_options.html.twig' %}
{% endblock %}
================================================
FILE: templates/tag/front.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
#{{ tag }} - {{ 'threads'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-tag-entries{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'tag/_options.html.twig' %}
{% include 'entry/_list.html.twig' %}
{% endblock %}
================================================
FILE: templates/tag/overview.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
#{{ tag }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-tag-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% if app.user and app.user.admin %}
{{ 'admin_panel'|trans }}
{% endif %}
{% endblock %}
{% block body %}
{% include 'tag/_options.html.twig' %}
{% endblock %}
================================================
FILE: templates/tag/people.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
#{{ tag }} - {{ 'people'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-tag-people page-people{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'tag/_options.html.twig' %}
{{ 'people_local'|trans }}
{% for user in local %}
{{ component('user_box', {user: user}) }}
{% endfor %}
{% if not local|length %}
{% endif %}
{{ 'people_federated'|trans }}
{% for user in federated %}
{{ component('user_box', {user: user}) }}
{% endfor %}
{% if not federated|length %}
{% endif %}
{% endblock %}
================================================
FILE: templates/tag/posts.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
#{{ tag }} - {{ 'posts'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-tag-posts{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'tag/_options.html.twig' %}
{% endblock %}
================================================
FILE: templates/user/2fa.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'Two factor authentication'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-2fa{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'two_factor_authentication'|trans }}
{% endblock %}
================================================
FILE: templates/user/_admin_panel.html.twig
================================================
{% if (is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR')) and app.user != user and not user.admin() %}
{{ 'admin_panel'|trans }}
{% if user.apId is same as null and not user.isVerified and is_granted('ROLE_ADMIN') %}
{% endif %}
{% if user.isTotpAuthenticationEnabled and is_granted('ROLE_ADMIN') %}
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
{% if user.id is not same as app.user.id and not user.admin %}
{% if user.markedForDeletionAt is same as null and user.apId is same as null %}
{% elseif user.markedForDeletionAt is not same as null and user.apId is same as null %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
================================================
FILE: templates/user/_boost_list.html.twig
================================================
{% include 'layout/_subject_list.html.twig' with {entryCommentAttributes: {showMagazineName: false}} %}
================================================
FILE: templates/user/_federated_info.html.twig
================================================
{% if user.apId %}
{% if is_instance_of_user_banned(user) %}
{{ 'user_instance_defederated_info'|trans }}
{% endif %}
{% endif %}
================================================
FILE: templates/user/_info.html.twig
================================================
{{ user.title ?? user.username|username }}
{% if is_route_name_starts_with('user_settings') %}
{% if user.avatar %}
{% endif %}
{{ user.username|username }}{% if not user.apId %}@{{ kbin_domain() }}{% endif %}
{% if user.apManuallyApprovesFollowers is same as true %}
{% endif %}
{{ component('user_actions', {user: user}) }}
{% endif %}
{{ 'joined'|trans }}: {{ component('date', {date: user.createdAt}) }}
{{ 'cake_day'|trans }}: {{ user.createdAt|format_date('short', '', null, 'gregorian', mbin_lang()) }}
{% if app.user is defined and app.user is not null and app.user.admin() %}
{% set attitude = get_user_attitude(user) %}
{{ 'attitude'|trans }}:
{% if attitude > 0 %}
{{ attitude|number_format(2) }}%
{% else %}
-
{% endif %}
{% if user.apId is not null %}
{{ 'last_updated'|trans }}: {{ component('date', {date: user.apFetchedAt}) }}
{% endif %}
{% 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 %}
{% endif %}
{%- set TYPE_ENTRY = constant('App\\Repository\\ReputationRepository::TYPE_ENTRY') -%}
{{ 'reputation_points'|trans }}: {{ get_reputation_total(user) }}
{{ 'moderated'|trans }}: {{ count_user_moderated(user) }}
{% if app.user is not same as user and not is_instance_of_user_banned(user) %}
{{ 'send_message'|trans }}
{% endif %}
================================================
FILE: templates/user/_list.html.twig
================================================
{%- set V_TRUE = constant('App\\Controller\\User\\ThemeSettingsController::TRUE') -%}
{%- set V_FALSE = constant('App\\Controller\\User\\ThemeSettingsController::FALSE') -%}
{%- set SHOW_USER_FULLNAME = app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::MBIN_SHOW_USER_DOMAIN'), V_FALSE) -%}
{% if users|length %}
{% if view is same as 'cards'|trans|lower %}
{% for user in users %}
{{ component('user', {user: user, showMeta: false, showInfo: false}) }}
{% endfor %}
{% elseif view is same as 'columns'|trans|lower %}
{% for user in users %}
{% if user.avatar %}
{% endif %}
{% endfor %}
{% else %}
{{ 'name'|trans }}
{{ 'threads'|trans }}
{{ 'comments'|trans }}
{{ 'posts'|trans }}
{% for u in users %}
{{ component('user_inline', { user: u, stretchedLink: true, showAvatar: true, showNewIcon: true, fullName: SHOW_USER_FULLNAME is same as V_TRUE}) }}
{{ u.entries|length }}
{{ u.entryComments|length }}
{{ u.posts|length + u.postComments|length }}
{{ component('user_actions', {user: u}) }}
{% endfor %}
{% endif %}
{% else %}
{% endif %}
================================================
FILE: templates/user/_options.html.twig
================================================
================================================
FILE: templates/user/_user_popover.html.twig
================================================
{{ form_start(form) }}
{{ form_row(form.body, {label: 'note'}) }}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary', 'data-action': ''}, row_attr: {class: 'float-end'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/user/_visibility_info.html.twig
================================================
{% if user is defined and user and user.visibility is same as 'trashed' %}
{{ 'account_is_suspended'|trans }}
{% endif %}
================================================
FILE: templates/user/comments.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'comments'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include 'entry/comment/_list.html.twig' with {showNested: false} %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/consent.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'oauth.consent.title'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-login{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ app_name }} - {{ 'oauth.consent.grant_permissions'|trans }}
{% include 'layout/_flash.html.twig' %}
{% if image %}
{% endif %}
{{ app_name }} {{ 'oauth.consent.app_requesting_permissions'|trans }}:
{% for scope in scopes %}
{{ scope|trans }}
{% endfor %}
{% if has_existing_scopes %}
{{ app_name }} {{ 'oauth.consent.app_has_permissions'|trans }}:
{% for scope in existing_scopes %}
{{ scope|trans }}
{% endfor %}
{% endif %}
{{ 'oauth.consent.to_allow_access'|trans }}
{% endblock %}
================================================
FILE: templates/user/entries.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'threads'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include 'entry/_list.html.twig' %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/followers.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'followers'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include 'layout/_user_activity_list.html.twig' with {list: users, actor: 'follower'} %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/following.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'following'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include 'layout/_user_activity_list.html.twig' with {list: users, actor: 'following'} %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/login.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'login'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-login{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'login'|trans }}
{% include 'layout/_flash.html.twig' %}
{% if mbin_sso_show_first() %}
{{ component('login_socials') }}
{% endif %}
{% if not mbin_sso_only_mode() %}
{% endif %}
{% if not mbin_sso_show_first() %}
{{ component('login_socials') }}
{% endif %}
{% if not mbin_sso_only_mode() %}
{{ component('user_form_actions', {showRegister: true, showPasswordReset: true, showResendEmail: true}) }}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/message.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'send_message'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-send-message{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/_options.html.twig') %}
{% include 'messages/_form_create.html.twig' %}
{% endblock %}
================================================
FILE: templates/user/moderated.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'moderated'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include('magazine/_list.html.twig') %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/overview.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'overview'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('layout/_flash.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/posts.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'posts'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/register.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'register'|trans }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-register{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ 'register'|trans }}
{% if mbin_sso_registrations_enabled() and mbin_sso_show_first() %}
{{ component('login_socials') }}
{% endif %}
{% if kbin_registrations_enabled() %}
{{ form_start(form) }}
{% for flash_error in app.flashes('verify_email_error') %}
{{ flash_error }}
{% endfor %}
{{ form_row(form.username, {
label: 'username',
}) }}
{% if do_new_users_need_approval() %}
{{ form_row(form.applicationText, {
label: 'application_text',
}) }}
{% endif %}
{{ form_row(form.email, {
label: 'email'
}) }}
{{ form_row(form.plainPassword, {
label: 'password'
}) }}
{% if kbin_captcha_enabled() %}
{{ form_row(form.captcha, {
label: false
}) }}
{% endif %}
{{ form_row(form.agreeTerms, {
translation_domain: false,
label: 'agree_terms'|trans({
'%terms_link_start%' : '
', '%terms_link_end%' : ' ',
'%policy_link_start%' : '
', '%policy_link_end%' : ' ',
}),
attr: {
'aria-label': 'agree_terms'|trans
},
row_attr: {
class: 'checkbox'
}
}) }}
{{ form_row(form.submit, {
label: 'register',
attr: {
class: 'btn btn__primary'
},
row_attr: {
class: 'button-flex-hf'
}
}) }}
{{ form_end(form) }}
{% else %}
{{ 'registration_disabled'|trans }}
{% endif %}
{% if mbin_sso_registrations_enabled() and not mbin_sso_show_first() %}
{{ component('login_socials') }}
{% endif %}
{{ component('user_form_actions', {showLogin: true, showPasswordReset: true, showResendEmail: true}) }}
{% endblock %}
================================================
FILE: templates/user/replies.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'replies'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-replies{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/reputation.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reputation_points'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% include('user/_admin_panel.html.twig') %}
{% endblock %}
{% block body %}
{%- set TYPE_ENTRY = constant('App\\Repository\\ReputationRepository::TYPE_ENTRY') -%}
{%- set TYPE_ENTRY_COMMENT = constant('App\\Repository\\ReputationRepository::TYPE_ENTRY_COMMENT') -%}
{%- set TYPE_POST = constant('App\\Repository\\ReputationRepository::TYPE_POST') -%}
{%- set TYPE_POST_COMMENT = constant('App\\Repository\\ReputationRepository::TYPE_POST_COMMENT') -%}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% if results|length %}
{% for subject in results %}
{{ subject.points }}
{{ subject.day >= date('-2 days') ? subject.day|ago : subject.day|date('Y-m-d') }}
{% endfor %}
{% endif %}
{% if(results.haveToPaginate is defined and results.haveToPaginate) %}
{{ pagerfanta(results, null, {'pageParameter':'[p]'}) }}
{% endif %}
{% if not results|length %}
{% endif %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/settings/2fa.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'two_factor_authentication'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-2fa{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{{ 'two_factor_authentication'|trans }}
{{ '2fa.enable'|trans }}
{{ '2fa.available_apps' | trans({
'%google_authenticator%': 'Google Authenticator ',
'%aegis%': 'Aegis ',
'%raivo%': 'Raivo '
}) | raw }}
{{ '2fa.manual_code_hint'|trans }}:
{% include 'user/settings/2fa_secret.html.twig' with {'secret': secret} %}
{{ '2fa.backup'|trans }}
{% include 'user/settings/_2fa_backup.html.twig' %}
{{ form_start(form) }}
{{ form_row(form.totpCode) }}
{{ form_row(form.currentPassword) }}
{{ form_row(form.submit, {label: '2fa.add', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/user/settings/2fa_backup.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'two_factor_backup'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-2fa{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{{ 'two_factor_backup_codes'|trans }}
{{ '2fa.backup'|trans }}
{% include 'user/settings/_2fa_backup.html.twig' %}
{% endblock %}
================================================
FILE: templates/user/settings/2fa_secret.html.twig
================================================
================================================
FILE: templates/user/settings/_2fa_backup.html.twig
================================================
{% for code in codes %}
{{ code }}
{% endfor %}
{{ '2fa.backup_codes.help' | trans | raw }}
{{ '2fa.backup_codes.recommendation' | trans }}
================================================
FILE: templates/user/settings/_options.html.twig
================================================
{%- set STATUS_PENDING = constant('App\\Entity\\Report::STATUS_PENDING') -%}
================================================
FILE: templates/user/settings/_stats_pills.html.twig
================================================
{%- set TYPE_CONTENT = constant('App\\Repository\\StatsRepository::TYPE_CONTENT') -%}
{%- set TYPE_VOTES = constant('App\\Repository\\StatsRepository::TYPE_VOTES') -%}
================================================
FILE: templates/user/settings/account_deletion.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'account_deletion_title'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-account-deletion{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'account_deletion_title'|trans }}
{{ 'account_deletion_description'|trans }}
{{ form_start(form) }}
{{ form_row(form.currentPassword, {label: 'current_password'}) }}
{{ form_row(form.instantDelete, {label: 'account_deletion_immediate', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.submit, { label: 'account_deletion_button', attr: { class: 'btn btn__danger' } }) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/user/settings/block_domains.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'blocked'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-block-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/block_pills.html.twig' %}
{% include 'layout/_domain_activity_list.html.twig' with {list: domains, actor: 'domain'} %}
{% endblock %}
================================================
FILE: templates/user/settings/block_magazines.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'blocked'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-block-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/block_pills.html.twig' %}
{% include 'layout/_magazine_activity_list.html.twig' with {list: magazines, actor: 'magazine'} %}
{% endblock %}
================================================
FILE: templates/user/settings/block_pills.html.twig
================================================
================================================
FILE: templates/user/settings/block_users.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'blocked'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-block-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/block_pills.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: users, actor: 'blocked'} %}
{% endblock %}
================================================
FILE: templates/user/settings/email.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'email'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-email{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'profile'|trans }}
{% if not app.user.SsoControlled() %}
{{ 'change_email'|trans }}
{{ form_start(form) }}
{{ form_row(form.email, {label: 'old_email', value: app.user.email, attr: {disabled: 'disabled'}}) }}
{{ form_row(form.newEmail, {label: 'new_email'}) }}
{{ form_row(form.currentPassword, {label: 'password'}) }}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% else %}
{{ 'email'|trans }}
{{ 'old_email'|trans }}
{{ app.user.email }}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/settings/filter_lists.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'filter_lists'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-filter-lists{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'filter_lists'|trans }}
{% for list in app.user.filterLists %}
{{ component('filter_list', {list: list}) }}
{% endfor %}
{% endblock %}
================================================
FILE: templates/user/settings/filter_lists_create.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'filter_lists'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-filter-lists{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'filter_lists'|trans }}
{{ include('user/settings/filter_lists_form.html.twig', {btn_label: 'add'|trans }) }}
{% endblock %}
================================================
FILE: templates/user/settings/filter_lists_edit.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'filter_lists'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-filter-lists{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'filter_lists'|trans }}
{{ include('user/settings/filter_lists_form.html.twig', {btn_label: 'save'|trans }) }}
{% endblock %}
================================================
FILE: templates/user/settings/filter_lists_form.html.twig
================================================
{{ form_start(form) }}
{{ form_row(form.name, {label: 'name'}) }}
{{ form_label(form.expirationDate, 'expiration_date') }}
{{ form_widget(form.expirationDate, {'attr': {'class': 'form-control', 'style': 'padding: 1rem .5rem;', 'title': 'expiration_date'|trans}}) }}
{{ 'filter_lists_where_to_filter'|trans }}:
{{ form_label(form.feeds) }}
{{ form_widget(form.feeds) }}
{{ form_help(form.feeds) }}
{{ form_label(form.comments) }}
{{ form_widget(form.comments) }}
{{ form_help(form.comments) }}
{{ form_label(form.profile) }}
{{ form_widget(form.profile) }}
{{ form_help(form.profile) }}
{{ form_label(form.words) }} {{ 'filter_lists_word_exact_match_help'|trans }}
{{ form_widget(form.words) }}
{{ '+' }}
{{ form_row(form.submit, {label: btn_label, attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
================================================
FILE: templates/user/settings/general.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'general'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-general{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'general'|trans }}
{{ form_start(form) }}
{{ 'appearance'|trans }}
{{ form_row(form.homepage, {label: 'homepage'}) }}
{{ form_row(form.frontDefaultContent, {label: 'front_default_content'}) }}
{{ form_row(form.frontDefaultSort, {label: 'front_default_sort'}) }}
{{ form_row(form.commentDefaultSort, {label: 'comment_default_sort'}) }}
{{ form_row(form.preferredLanguages, {label: 'preferred_languages'}) }}
{{ form_row(form.featuredMagazines, {label: 'featured_magazines'}) }}
{{ form_row(form.customCss, {label: 'custom_css', row_attr: {class: 'textarea'}}) }}
{{ form_row(form.ignoreMagazinesCustomCss, {label: 'ignore_magazines_custom_css', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.hideAdult, {label: 'hide_adult', row_attr: {class: 'checkbox'}}) }}
{{ form_label(form.showFollowingBoosts, 'show_boost_following_label') }}
{{ form_widget(form.showFollowingBoosts) }}
{{ form_help(form.showFollowingBoosts) }}
{{ 'writing'|trans }}
{{ form_row(form.addMentionsEntries, {label: 'add_mentions_entries', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.addMentionsPosts, {label: 'add_mentions_posts', row_attr: {class: 'checkbox'}}) }}
{{ 'privacy'|trans }}
{{ form_row(form.directMessageSetting, {label: 'direct_message_setting_label'}) }}
{{ form_row(form.showProfileSubscriptions, {label: 'show_profile_subscriptions', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.showProfileFollowings, {label: 'show_profile_followings', row_attr: {class: 'checkbox'}}) }}
{{ form_label(form.discoverable, 'discoverable') }}
{{ form_widget(form.discoverable) }}
{{ form_help(form.discoverable) }}
{{ form_label(form.indexable, 'indexable_by_search_engines') }}
{{ form_widget(form.indexable) }}
{{ form_help(form.indexable) }}
{{ 'notifications'|trans }}
{{ form_row(form.notifyOnNewEntryReply, {label: 'notify_on_new_entry_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewEntryCommentReply, {label: 'notify_on_new_entry_comment_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewPostReply, {label: 'notify_on_new_post_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewPostCommentReply, {label: 'notify_on_new_post_comment_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewEntry, {label: 'notify_on_new_entry', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewPost, {label: 'notify_on_new_posts', row_attr: {class: 'checkbox'}}) }}
{% if app.user.admin or app.user.moderator %}
{{ form_row(form.notifyOnUserSignup, {label: 'notify_on_user_signup', row_attr: {class: 'checkbox'}}) }}
{% endif %}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/user/settings/password.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'password'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-password{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ 'password_and_2fa'|trans }}
{{ 'change_password'|trans }}
{{ form_start(form) }}
{{ form_row(form.currentPassword) }}
{{ form_row(form.totpCode, {required: has2fa}) }}
{{ form_row(form.plainPassword) }}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{{ 'two_factor_authentication'|trans }}
{% if has2fa %}
{{ form_start(disable2faForm, {action: path('user_settings_2fa_disable'), attr: {'data-action': "confirmation#ask", 'data-confirmation-message-param': 'are_you_sure'|trans}}) }}
{{ form_row(disable2faForm.currentPassword) }}
{{ form_row(disable2faForm.totpCode, {required: has2fa}) }}
{{ form_row(disable2faForm.submit, {label: '2fa.disable', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(disable2faForm) }}
{{ '2fa.backup-create.help' | trans }}
{{ form_start(regenerateBackupCodes, {action: path('user_settings_2fa_backup'), attr: {'data-action': "confirmation#ask", 'data-confirmation-message-param': 'are_you_sure'|trans}}) }}
{{ form_row(regenerateBackupCodes.currentPassword) }}
{{ form_row(regenerateBackupCodes.totpCode, {required: has2fa}) }}
{{ form_row(regenerateBackupCodes.submit, {label: '2fa.backup-create.label', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(regenerateBackupCodes) }}
{% else %}
{% endif %}
{% endblock %}
================================================
FILE: templates/user/settings/profile.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'profile'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-profile{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'layout/_flash.html.twig' %}
{{ component('user_box', {
user: app.user,
stretchedLink: false
}) }}
{{ 'profile'|trans }}
{{ form_start(form) }}
{{ form_row(form.username, {label: 'username', attr: {
'data-controller': 'input-length autogrow',
'data-entry-link-create-target': 'user_about',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\DTO\\UserDto::MAX_USERNAME_LENGTH')
}}) }}
{{ form_row(form.title, {label: 'displayname', attr: {
'data-controller': 'input-length autogrow',
'data-entry-link-create-target': 'user_about',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\DTO\\UserDto::MAX_USERNAME_LENGTH')
}}) }}
{{ component('editor_toolbar', {id: 'user_basic_about'}) }}
{{ form_row(form.about, {label: false, attr: {
placeholder: 'about',
'data-controller': 'input-length rich-textarea autogrow',
'data-entry-link-create-target': 'user_about',
'data-action' : 'input-length#updateDisplay',
'data-input-length-max-value' : constant('App\\DTO\\UserDto::MAX_ABOUT_LENGTH')
}}) }}
{{ form_row(form.avatar, {label: 'avatar'}) }}
{% if app.user.avatar is not same as null %}
{% endif %}
{{ form_row(form.cover, {label: 'cover'}) }}
{% if app.user.cover is not same as null %}
{% endif %}
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
{{ form_end(form) }}
{% endblock %}
================================================
FILE: templates/user/settings/reports.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reports'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-reports{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{{ 'reports'|trans }}
{{ component('report_list', {reports: reports, routeName: 'user_settings_reports'}) }}
{% endblock %}
================================================
FILE: templates/user/settings/stats.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'reports'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-reports{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/_stats_pills.html.twig' %}
{% include 'stats/_filters.html.twig' %}
{{ render_chart(chart) }}
{% endblock %}
================================================
FILE: templates/user/settings/sub_domains.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'subscriptions'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-sub-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/sub_pills.html.twig' %}
{% include 'layout/_domain_activity_list.html.twig' with {list: domains, actor: 'domain'} %}
{% endblock %}
================================================
FILE: templates/user/settings/sub_magazines.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'subscriptions'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-sub-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include('user/settings/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/sub_pills.html.twig' %}
{% include 'layout/_magazine_activity_list.html.twig' with {list: magazines, actor: 'magazine'} %}
{% endblock %}
================================================
FILE: templates/user/settings/sub_pills.html.twig
================================================
================================================
FILE: templates/user/settings/sub_users.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'subscriptions'|trans }} - {{ app.user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-settings page-settings-sub-magazines{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{% include 'user/settings/_options.html.twig' %}
{% include('user/_visibility_info.html.twig') %}
{% include 'user/settings/sub_pills.html.twig' %}
{% include 'layout/_user_activity_list.html.twig' with {list: users, actor: 'following'} %}
{% endblock %}
================================================
FILE: templates/user/subscriptions.html.twig
================================================
{% extends 'base.html.twig' %}
{%- block title -%}
{{- 'subscriptions'|trans }} - {{ user.username|username(false) }} - {{ parent() -}}
{%- endblock -%}
{% block mainClass %}page-user page-user-overview{% endblock %}
{% block header_nav %}
{% endblock %}
{% block sidebar_top %}
{% endblock %}
{% block body %}
{{ component('user_box', {
user: user,
stretchedLink: false
}) }}
{% include('user/_options.html.twig') %}
{% include('user/_visibility_info.html.twig') %}
{% include('user/_federated_info.html.twig') %}
{% if user.visibility is same as 'visible' or is_granted('ROLE_ADMIN') or is_granted('ROLE_MODERATOR') %}
{% include 'layout/_magazine_activity_list.html.twig' with {list: magazines} %}
{% endif %}
{% endblock %}
================================================
FILE: tests/ActivityPubJsonDriver.php
================================================
scrub($data);
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."\n";
}
public function match($expected, $actual): void
{
if (\is_string($actual)) {
$actual = json_decode($actual, false, 512, JSON_THROW_ON_ERROR);
}
$actual = $this->scrub($actual);
$expected = json_decode($expected, false, 512, JSON_THROW_ON_ERROR);
Assert::assertJsonStringEqualsJsonString(json_encode($expected), json_encode($actual));
}
protected function scrub(mixed $data): mixed
{
if (\is_array($data)) {
return $this->scrubArray($data);
} elseif (\is_object($data)) {
return $this->scrubObject($data);
}
return $this;
}
protected function scrubArray(array $data): array
{
if (isset($data['id'])) {
$data['id'] = 'SCRUBBED_ID';
}
if (isset($data['type']) && 'Note' === $data['type'] && isset($data['url'])) {
$data['url'] = 'SCRUBBED_ID';
}
if (isset($data['inReplyTo'])) {
$data['inReplyTo'] = 'SCRUBBED_ID';
}
if (isset($data['published'])) {
$data['published'] = 'SCRUBBED_DATE';
}
if (isset($data['updated'])) {
$data['updated'] = 'SCRUBBED_DATE';
}
if (isset($data['publicKey'])) {
$data['publicKey'] = 'SCRUBBED_KEY';
}
if (isset($data['object']) && \is_string($data['object'])) {
$data['object'] = 'SCRUBBED_ID';
}
if (isset($data['object']) && (\is_array($data['object']) || \is_object($data['object']))) {
$data['object'] = $this->scrub($data['object']);
}
if (isset($data['orderedItems']) && \is_array($data['orderedItems'])) {
$items = [];
foreach ($data['orderedItems'] as $item) {
$items[] = $this->scrub($item);
}
$data['orderedItems'] = $items;
}
return $data;
}
protected function scrubObject(object $data): object
{
if (isset($data->id)) {
$data->id = 'SCRUBBED_ID';
}
if (isset($data->type) && 'Note' === $data->type && isset($data->url)) {
$data->url = 'SCRUBBED_ID';
}
if (isset($data->inReplyTo)) {
$data->inReplyTo = 'SCRUBBED_ID';
}
if (isset($data->published)) {
$data->published = 'SCRUBBED_DATE';
}
if (isset($data->updated)) {
$data->updated = 'SCRUBBED_DATE';
}
if (isset($data->publicKey)) {
$data->publicKey = 'SCRUBBED_KEY';
}
if (isset($data->object) && \is_string($data->object)) {
$data->object = 'SCRUBBED_ID';
}
if (isset($data->object) && (\is_array($data->object) || \is_object($data->object))) {
$data->object = $this->scrub($data->object);
}
return $data;
}
}
================================================
FILE: tests/ActivityPubTestCase.php
================================================
owner = $this->getUserByUsername('owner', addImage: false);
$this->magazine = $this->getMagazineByName('test', $this->owner);
$this->user = $this->getUserByUsername('user', addImage: false);
$this->personFactory = $this->getService(PersonFactory::class);
$this->groupFactory = $this->getService(GroupFactory::class);
$this->instanceFactory = $this->getService(InstanceFactory::class);
$this->entryCommentNoteFactory = $this->getService(EntryCommentNoteFactory::class);
$this->postNoteFactory = $this->getService(PostNoteFactory::class);
$this->postCommentNoteFactory = $this->getService(PostCommentNoteFactory::class);
$this->addRemoveFactory = $this->getService(AddRemoveFactory::class);
$this->createWrapper = $this->getService(CreateWrapper::class);
$this->updateWrapper = $this->getService(UpdateWrapper::class);
$this->deleteWrapper = $this->getService(DeleteWrapper::class);
$this->likeWrapper = $this->getService(LikeWrapper::class);
$this->followWrapper = $this->getService(FollowWrapper::class);
$this->announceWrapper = $this->getService(AnnounceWrapper::class);
$this->undoWrapper = $this->getService(UndoWrapper::class);
$this->followResponseWrapper = $this->getService(FollowResponseWrapper::class);
$this->flagFactory = $this->getService(FlagFactory::class);
$this->blockFactory = $this->getService(BlockFactory::class);
$this->lockFactory = $this->getService(LockFactory::class);
$this->userFollowRequestRepository = $this->getService(UserFollowRequestRepository::class);
$this->apMarkdownConverter = $this->getService(MarkdownConverter::class);
}
/**
* @template T
*
* @param class-string $className
*
* @return T
*/
private function getService(string $className)
{
return $this->getContainer()->get($className);
}
protected function getDefaultUuid(): Uuid
{
return new Uuid('00000000-0000-0000-0000-000000000000');
}
protected function getSnapshotDirectory(): string
{
return \dirname((new \ReflectionClass($this))->getFileName()).
DIRECTORY_SEPARATOR.
'JsonSnapshots';
}
}
================================================
FILE: tests/FactoryTrait.php
================================================
favouriteManager;
$favManager->toggle($user, $subject);
} else {
$voteManager = $this->voteManager;
$voteManager->vote($choice, $subject, $user);
}
}
protected function loadExampleMagazines(): void
{
$this->loadExampleUsers();
foreach ($this->provideMagazines() as $data) {
$this->createMagazine($data['name'], $data['title'], $data['user'], $data['isAdult'], $data['description']);
}
}
protected function loadExampleUsers(): void
{
foreach ($this->provideUsers() as $data) {
$this->createUser($data['username'], $data['email'], $data['password']);
}
}
private function provideUsers(): iterable
{
yield [
'username' => 'adminUser',
'password' => 'adminUser123',
'email' => 'adminUser@example.com',
'type' => 'Person',
];
yield [
'username' => 'JohnDoe',
'password' => 'JohnDoe123',
'email' => 'JohnDoe@example.com',
'type' => 'Person',
];
}
private function createUser(string $username, ?string $email = null, ?string $password = null, string $type = 'Person', $active = true, $hideAdult = true, $about = null, $addImage = true): User
{
$user = new User($email ?: $username.'@example.com', $username, $password ?: 'secret', $type);
$user->isVerified = $active;
$user->notifyOnNewEntry = true;
$user->notifyOnNewEntryReply = true;
$user->notifyOnNewEntryCommentReply = true;
$user->notifyOnNewPost = true;
$user->notifyOnNewPostReply = true;
$user->notifyOnNewPostCommentReply = true;
$user->showProfileFollowings = true;
$user->showProfileSubscriptions = true;
$user->hideAdult = $hideAdult;
$user->apDiscoverable = true;
$user->about = $about;
$user->apIndexable = true;
if ($addImage) {
$user->avatar = $this->createImage(bin2hex(random_bytes(20)).'.png');
}
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->users->add($user);
return $user;
}
public function createMessage(User $to, User $from, string $content): Message
{
$thread = $this->createMessageThread($to, $from, $content);
/** @var Message $message */
$message = $thread->messages->get(0);
return $message;
}
public function createMessageThread(User $to, User $from, string $content): MessageThread
{
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = $content;
return $messageManager->toThread($dto, $from, $to);
}
public static function createOAuth2AuthCodeClient(): void
{
/** @var ClientManagerInterface $manager */
$manager = self::getContainer()->get(ClientManagerInterface::class);
$client = new Client('/kbin Test Client', 'testclient', 'testsecret');
$client->setDescription('An OAuth2 client for testing purposes');
$client->setContactEmail('test@kbin.test');
$client->setScopes(...array_map(fn (string $scope) => new Scope($scope), OAuth2ClientDto::AVAILABLE_SCOPES));
$client->setGrants(new Grant('authorization_code'), new Grant('refresh_token'));
$client->setRedirectUris(new RedirectUri('https://localhost:3001'));
$manager->save($client);
}
public static function createOAuth2PublicAuthCodeClient(): void
{
/** @var ClientManagerInterface $manager */
$manager = self::getContainer()->get(ClientManagerInterface::class);
$client = new Client('/kbin Test Client', 'testpublicclient', null);
$client->setDescription('An OAuth2 public client for testing purposes');
$client->setContactEmail('test@kbin.test');
$client->setScopes(...array_map(fn (string $scope) => new Scope($scope), OAuth2ClientDto::AVAILABLE_SCOPES));
$client->setGrants(new Grant('authorization_code'), new Grant('refresh_token'));
$client->setRedirectUris(new RedirectUri('https://localhost:3001'));
$manager->save($client);
}
public static function createOAuth2ClientCredsClient(): void
{
/** @var ClientManagerInterface $clientManager */
$clientManager = self::getContainer()->get(ClientManagerInterface::class);
/** @var UserManager $userManager */
$userManager = self::getContainer()->get(UserManager::class);
$client = new Client('/kbin Test Client', 'testclient', 'testsecret');
$userDto = new UserDto();
$userDto->username = 'test_bot';
$userDto->email = 'test@kbin.test';
$userDto->plainPassword = hash('sha512', random_bytes(32));
$userDto->isBot = true;
$user = $userManager->create($userDto, false, false, true);
$client->setUser($user);
$client->setDescription('An OAuth2 client for testing purposes');
$client->setContactEmail('test@kbin.test');
$client->setScopes(...array_map(fn (string $scope) => new Scope($scope), OAuth2ClientDto::AVAILABLE_SCOPES));
$client->setGrants(new Grant('client_credentials'));
$client->setRedirectUris(new RedirectUri('https://localhost:3001'));
$clientManager->save($client);
}
private function provideMagazines(): iterable
{
yield [
'name' => 'acme',
'title' => 'Magazyn polityczny',
'user' => $this->getUserByUsername('JohnDoe'),
'isAdult' => false,
'description' => 'Foobar',
];
yield [
'name' => 'kbin',
'title' => 'kbin devlog',
'user' => $this->getUserByUsername('adminUser'),
'isAdult' => false,
'description' => 'development process in detail',
];
yield [
'name' => 'adult',
'title' => 'Adult only',
'user' => $this->getUserByUsername('JohnDoe'),
'isAdult' => true,
'description' => 'Not for kids',
];
yield [
'name' => 'starwarsmemes@republic.new',
'title' => 'starwarsmemes@republic.new',
'user' => $this->getUserByUsername('adminUser'),
'isAdult' => false,
'description' => "It's a trap",
];
}
protected function getUserByUsername(string $username, bool $isAdmin = false, bool $hideAdult = true, ?string $about = null, bool $active = true, bool $isModerator = false, bool $addImage = true, ?string $email = null): User
{
$user = $this->users->filter(fn (User $user) => $user->getUsername() === $username)->first();
if ($user) {
return $user;
}
$user = $this->createUser($username, email: $email, active: $active, hideAdult: $hideAdult, about: $about, addImage: $addImage);
if ($isAdmin) {
$user->roles = ['ROLE_ADMIN'];
} elseif ($isModerator) {
$user->roles = ['ROLE_MODERATOR'];
}
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
protected function setAdmin(User $user): void
{
$user->roles = ['ROLE_ADMIN'];
$manager = $this->entityManager;
$manager->persist($user);
$manager->flush();
$manager->refresh($user);
}
private function createMagazine(
string $name,
?string $title = null,
?User $user = null,
bool $isAdult = false,
?string $description = null,
): Magazine {
$dto = new MagazineDto();
$dto->name = $name;
$dto->title = $title ?? 'Magazine title';
$dto->isAdult = $isAdult;
$dto->description = $description;
if (str_contains($name, '@')) {
[$name, $host] = explode('@', $name);
$dto->apId = $name;
$dto->apProfileId = "https://{$host}/m/{$name}";
}
$newMod = $user ?? $this->getUserByUsername('JohnDoe');
$this->entityManager->persist($newMod);
$magazine = $this->magazineManager->create($dto, $newMod);
$this->entityManager->persist($magazine);
$this->magazines->add($magazine);
return $magazine;
}
protected function loadNotificationsFixture()
{
$owner = $this->getUserByUsername('owner');
$magazine = $this->getMagazineByName('acme', $owner);
$actor = $this->getUserByUsername('actor');
$regular = $this->getUserByUsername('JohnDoe');
$entry = $this->getEntryByTitle('test', null, 'test', $magazine, $actor);
$comment = $this->createEntryComment('test', $entry, $regular);
$this->entryCommentManager->delete($owner, $comment);
$this->entryManager->delete($owner, $entry);
$post = $this->createPost('test', $magazine, $actor);
$comment = $this->createPostComment('test', $post, $regular);
$this->postCommentManager->delete($owner, $comment);
$this->postManager->delete($owner, $post);
$this->magazineManager->ban(
$magazine,
$actor,
$owner,
MagazineBanDto::create('test', new \DateTimeImmutable('+1 day'))
);
}
protected function getMagazineByName(string $name, ?User $user = null, bool $isAdult = false): Magazine
{
$magazine = $this->magazines->filter(fn (Magazine $magazine) => $magazine->name === $name)->first();
return $magazine ?: $this->createMagazine($name, $name, $user, $isAdult);
}
protected function getMagazineByNameNoRSAKey(string $name, ?User $user = null, bool $isAdult = false): Magazine
{
$magazine = $this->magazines->filter(fn (Magazine $magazine) => $magazine->name === $name)->first();
if ($magazine) {
return $magazine;
}
$dto = new MagazineDto();
$dto->name = $name;
$dto->title = $title ?? 'Magazine title';
$dto->isAdult = $isAdult;
if (str_contains($name, '@')) {
[$name, $host] = explode('@', $name);
$dto->apId = $name;
$dto->apProfileId = "https://{$host}/m/{$name}";
}
$factory = $this->magazineFactory;
$magazine = $factory->createFromDto($dto, $user ?? $this->getUserByUsername('JohnDoe'));
$magazine->apId = $dto->apId;
$magazine->apProfileId = $dto->apProfileId;
$magazine->apDiscoverable = true;
if (!$dto->apId) {
$urlGenerator = $this->urlGenerator;
$magazine->publicKey = 'fakepublic';
$magazine->privateKey = 'fakeprivate';
$magazine->apProfileId = $urlGenerator->generate(
'ap_magazine',
['name' => $magazine->name],
UrlGeneratorInterface::ABSOLUTE_URL
);
}
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$manager = $this->magazineManager;
$manager->subscribe($magazine, $user ?? $this->getUserByUsername('JohnDoe'));
$this->magazines->add($magazine);
return $magazine;
}
protected function getEntryByTitle(
string $title,
?string $url = null,
?string $body = null,
?Magazine $magazine = null,
?User $user = null,
?ImageDto $image = null,
string $lang = 'en',
): Entry {
$entry = $this->entries->filter(fn (Entry $entry) => $entry->title === $title)->first();
if (!$entry) {
$magazine = $magazine ?? $this->getMagazineByName('acme');
$user = $user ?? $this->getUserByUsername('JohnDoe');
$entry = $this->createEntry($title, $magazine, $user, $url, $body, $image, $lang);
}
return $entry;
}
protected function createEntry(
string $title,
Magazine $magazine,
User $user,
?string $url = null,
?string $body = 'Test entry content',
?ImageDto $imageDto = null,
string $lang = 'en',
): Entry {
$manager = $this->entryManager;
$dto = new EntryDto();
$dto->magazine = $magazine;
$dto->title = $title;
$dto->user = $user;
$dto->url = $url;
$dto->body = $body;
$dto->lang = $lang;
$dto->image = $imageDto;
$entry = $manager->create($dto, $user);
$this->entries->add($entry);
return $entry;
}
public function createEntryComment(
string $body,
?Entry $entry = null,
?User $user = null,
?EntryComment $parent = null,
?ImageDto $imageDto = null,
string $lang = 'en',
): EntryComment {
$manager = $this->entryCommentManager;
$repository = $this->imageRepository;
if ($parent) {
$dto = (new EntryCommentDto())->createWithParent(
$entry ?? $this->getEntryByTitle('test entry content', 'https://kbin.pub'),
$parent,
$imageDto ? $repository->find($imageDto->id) : null,
$body
);
} else {
$dto = new EntryCommentDto();
$dto->entry = $entry ?? $this->getEntryByTitle('test entry content', 'https://kbin.pub');
$dto->body = $body;
$dto->image = $imageDto;
}
$dto->lang = $lang;
return $manager->create($dto, $user ?? $this->getUserByUsername('JohnDoe'));
}
public function createPost(string $body, ?Magazine $magazine = null, ?User $user = null, ?ImageDto $imageDto = null, string $lang = 'en'): Post
{
$manager = $this->postManager;
$dto = new PostDto();
$dto->magazine = $magazine ?: $this->getMagazineByName('acme');
$dto->body = $body;
$dto->lang = $lang;
$dto->image = $imageDto;
return $manager->create($dto, $user ?? $this->getUserByUsername('JohnDoe'));
}
public function createPostComment(string $body, ?Post $post = null, ?User $user = null, ?ImageDto $imageDto = null, ?PostComment $parent = null, string $lang = 'en'): PostComment
{
$manager = $this->postCommentManager;
$dto = new PostCommentDto();
$dto->post = $post ?? $this->createPost('test post content');
$dto->body = $body;
$dto->lang = $lang;
$dto->image = $imageDto;
$dto->parent = $parent;
return $manager->create($dto, $user ?? $this->getUserByUsername('JohnDoe'));
}
public function createPostCommentReply(string $body, ?Post $post = null, ?User $user = null, ?PostComment $parent = null): PostComment
{
$manager = $this->postCommentManager;
$dto = new PostCommentDto();
$dto->post = $post ?? $this->createPost('test post content');
$dto->body = $body;
$dto->lang = 'en';
$dto->parent = $parent ?? $this->createPostComment('test parent comment', $dto->post);
return $manager->create($dto, $user ?? $this->getUserByUsername('JohnDoe'));
}
public function createImage(string $fileName): Image
{
$imageRepo = $this->imageRepository;
$image = $imageRepo->findOneBy(['fileName' => $fileName]);
if ($image) {
return $image;
}
$image = new Image(
$fileName,
'/dev/random',
hash('sha256', $fileName),
100,
100,
null,
);
$this->entityManager->persist($image);
$this->entityManager->flush();
return $image;
}
public function createMessageNotification(?User $to = null, ?User $from = null): Notification
{
$messageManager = $this->messageManager;
$repository = $this->notificationRepository;
$dto = new MessageDto();
$dto->body = 'test message';
$messageManager->toThread($dto, $from ?? $this->getUserByUsername('JaneDoe'), $to ?? $this->getUserByUsername('JohnDoe'));
return $repository->findOneBy(['user' => $to ?? $this->getUserByUsername('JohnDoe')]);
}
protected function createInstancePages(): Site
{
$siteRepository = $this->siteRepository;
$entityManager = $this->entityManager;
$results = $siteRepository->findAll();
$site = null;
if (!\count($results)) {
$site = new Site();
} else {
$site = $results[0];
}
$site->about = 'about';
$site->contact = 'contact';
$site->faq = 'faq';
$site->privacyPolicy = 'privacyPolicy';
$site->terms = 'terms';
$entityManager->persist($site);
$entityManager->flush();
return $site;
}
/**
* Creates 5 modlog messages, one each of:
* * log_entry_deleted
* * log_entry_comment_deleted
* * log_post_deleted
* * log_post_comment_deleted
* * log_ban.
*/
public function createModlogMessages(): void
{
$magazineManager = $this->magazineManager;
$entryManager = $this->entryManager;
$entryCommentManager = $this->entryCommentManager;
$postManager = $this->postManager;
$postCommentManager = $this->postCommentManager;
$moderator = $this->getUserByUsername('moderator');
$magazine = $this->getMagazineByName('acme', $moderator);
$user = $this->getUserByUsername('user');
$post = $this->createPost('test post', $magazine, $user);
$entry = $this->getEntryByTitle('A title', body: 'test entry', magazine: $magazine, user: $user);
$postComment = $this->createPostComment('test comment', $post, $user);
$entryComment = $this->createEntryComment('test comment 2', $entry, $user);
$entryCommentManager->delete($moderator, $entryComment);
$entryManager->delete($moderator, $entry);
$postCommentManager->delete($moderator, $postComment);
$postManager->delete($moderator, $post);
$magazineManager->ban($magazine, $user, $moderator, MagazineBanDto::create('test ban', new \DateTimeImmutable('+12 hours')));
}
public function register($active = false): KernelBrowser
{
$crawler = $this->client->request('GET', '/register');
$this->client->submit(
$crawler->filter('form[name=user_register]')->selectButton('Register')->form(
[
'user_register[username]' => 'JohnDoe',
'user_register[email]' => 'johndoe@kbin.pub',
'user_register[plainPassword][first]' => 'secret',
'user_register[plainPassword][second]' => 'secret',
'user_register[agreeTerms]' => true,
]
)
);
if (302 === $this->client->getResponse()->getStatusCode()) {
$this->client->followRedirect();
}
self::assertResponseIsSuccessful();
if ($active) {
$user = self::getContainer()->get('doctrine')->getRepository(User::class)
->findOneBy(['username' => 'JohnDoe']);
$user->isVerified = true;
self::getContainer()->get('doctrine')->getManager()->flush();
self::getContainer()->get('doctrine')->getManager()->refresh($user);
}
return $this->client;
}
public function getKibbyImageDto(): ImageDto
{
return $this->getKibbyImageVariantDto('');
}
public function getKibbyFlippedImageDto(): ImageDto
{
return $this->getKibbyImageVariantDto('_flipped');
}
private function getKibbyImageVariantDto(string $suffix): ImageDto
{
$imageRepository = $this->imageRepository;
$imageFactory = $this->imageFactory;
if (!file_exists(\dirname($this->kibbyPath).'/copy')) {
if (!mkdir(\dirname($this->kibbyPath).'/copy')) {
throw new \Exception('The copy dir could not be created');
}
}
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = \dirname($this->kibbyPath).'/copy/'.bin2hex(random_bytes(32)).'.png';
$srcPath = \dirname($this->kibbyPath).'/'.basename($this->kibbyPath, '.png').$suffix.'.png';
if (!file_exists($srcPath)) {
throw new \Exception('For some reason the kibby image got deleted');
}
copy($srcPath, $tmpPath);
/** @var Image $image */
$image = $imageRepository->findOrCreateFromUpload(new UploadedFile($tmpPath, 'kibby_emoji.png', 'image/png'));
self::assertNotNull($image);
$image->altText = 'kibby';
$this->entityManager->persist($image);
$this->entityManager->flush();
$dto = $imageFactory->createDto($image);
assertNotNull($dto->id);
return $dto;
}
}
================================================
FILE: tests/Functional/ActivityPub/ActivityPubFunctionalTestCase.php
================================================
localDomain = $this->settingsManager->get('KBIN_DOMAIN');
$this->setupLocalActors();
$this->switchToRemoteDomain($this->remoteSubDomain);
$this->setUpRemoteSubscriber();
$this->entries = new ArrayCollection();
$this->magazines = new ArrayCollection();
$this->users = new ArrayCollection();
$this->switchToLocalDomain();
$this->switchToRemoteDomain($this->remoteDomain);
$this->setUpRemoteActors();
$this->setUpRemoteEntities();
$this->entries = new ArrayCollection();
$this->magazines = new ArrayCollection();
$this->users = new ArrayCollection();
$this->switchToLocalDomain();
$this->setUpLocalEntities();
$this->switchToRemoteDomain($this->remoteDomain);
$this->setUpLateRemoteEntities();
$this->switchToLocalDomain();
// foreach ($this->entitiesToRemoveAfterSetup as $entity) {
// $this->entityManager->remove($entity);
// }
for ($i = \sizeof($this->entitiesToRemoveAfterSetup) - 1; $i >= 0; --$i) {
$this->entityManager->remove($this->entitiesToRemoveAfterSetup[$i]);
}
$this->entries = new ArrayCollection();
$this->magazines = new ArrayCollection();
$this->users = new ArrayCollection();
$this->entityManager->flush();
$this->entityManager->clear();
$this->remoteSubscriber = $this->activityPubManager->findActorOrCreate("@remoteSubscriber@$this->remoteSubDomain");
$this->remoteSubscriber->publicKey = 'some public key';
$this->remoteMagazine = $this->activityPubManager->findActorOrCreate("!remoteMagazine@$this->remoteDomain");
$this->remoteMagazine->publicKey = 'some public key';
$this->remoteUser = $this->activityPubManager->findActorOrCreate("@remoteUser@$this->remoteDomain");
$this->remoteUser->publicKey = 'some public key';
$this->localMagazine = $this->magazineRepository->findOneByName('magazine');
$this->magazineManager->subscribe($this->localMagazine, $this->remoteSubscriber);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$this->entityManager->refresh($this->localMagazine);
$this->localUser = $this->userRepository->findOneByUsername('user');
}
protected function setupLocalActors(): void
{
$this->localUser = $this->getUserByUsername('user', addImage: false);
$this->localMagazine = $this->getMagazineByName('magazine', user: $this->localUser);
$this->entityManager->flush();
}
abstract public function setUpRemoteEntities(): void;
/**
* Override this method if you want to set up remote objects depending on you local entities.
*/
public function setUpLateRemoteEntities(): void
{
}
/**
* Override this method if you want to set up additional local entities.
*/
public function setUpLocalEntities(): void
{
}
protected function setUpRemoteActors(): void
{
$domain = $this->remoteDomain;
$username = 'remoteUser';
$this->remoteUser = $this->getUserByUsername($username, addImage: false);
$magazineName = 'remoteMagazine';
$this->remoteMagazine = $this->getMagazineByName($magazineName, user: $this->remoteUser);
$this->registerActor($this->remoteMagazine, $domain, true);
$this->registerActor($this->remoteUser, $domain, true);
}
protected function setUpRemoteSubscriber(): void
{
$domain = $this->remoteSubDomain;
$username = 'remoteSubscriber';
$this->remoteSubscriber = $this->getUserByUsername($username, addImage: false);
$this->registerActor($this->remoteSubscriber, $domain, true);
}
protected function registerActor(ActivityPubActorInterface $actor, string $domain, bool $removeAfterSetup = false): void
{
if ($actor instanceof User) {
$json = $this->personFactory->create($actor);
} elseif ($actor instanceof Magazine) {
$json = $this->groupFactory->create($actor);
} else {
$class = \get_class($actor);
throw new \LogicException("tests do not support actors of type $class");
}
$this->testingApHttpClient->actorObjects[$json['id']] = $json;
$username = $json['preferredUsername'];
$userEvent = new WebfingerResponseEvent(new JsonRd(), "acct:$username@$domain", ['account' => $username]);
$this->eventDispatcher->dispatch($userEvent);
$realDomain = \sprintf(WebFingerFactory::WEBFINGER_URL, 'https', $domain, '', "$username@$domain");
$this->testingApHttpClient->webfingerObjects[$realDomain] = $userEvent->jsonRd->toArray();
if ($removeAfterSetup) {
$this->entitiesToRemoveAfterSetup[] = $actor;
}
}
protected function switchToRemoteDomain($domain): void
{
$this->prev = $this->settingsManager->get('KBIN_DOMAIN');
$this->settingsManager->set('KBIN_DOMAIN', $domain);
$context = $this->router->getContext();
$context->setHost($domain);
}
protected function switchToLocalDomain(): void
{
if (null === $this->prev) {
return;
}
$context = $this->router->getContext();
$this->settingsManager->set('KBIN_DOMAIN', $this->prev);
$context->setHost($this->prev);
$this->prev = null;
}
/**
* @param callable(Entry $entry):void|null $entryCreateCallback
*/
protected function createRemoteEntryInRemoteMagazine(Magazine $magazine, User $user, ?callable $entryCreateCallback = null): array
{
$entry = $this->getEntryByTitle('remote entry', magazine: $magazine, user: $user);
$json = $this->pageFactory->create($entry, $this->tagLinkRepository->getTagsOfContent($entry));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($entry);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$announceActivity = $this->announceWrapper->build($magazine, $createActivity);
$announce = $this->activityJsonBuilder->buildActivityJson($announceActivity);
$this->testingApHttpClient->activityObjects[$announce['id']] = $announce;
if (null !== $entryCreateCallback) {
$entryCreateCallback($entry);
}
$this->entitiesToRemoveAfterSetup[] = $announceActivity;
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $entry;
return $announce;
}
/**
* @param callable(EntryComment $entry):void|null $entryCommentCreateCallback
*/
protected function createRemoteEntryCommentInRemoteMagazine(Magazine $magazine, User $user, ?callable $entryCommentCreateCallback = null): array
{
$entries = array_filter($this->entitiesToRemoveAfterSetup, fn ($item) => $item instanceof Entry);
$entry = $entries[array_key_first($entries)];
$comment = $this->createEntryComment('remote entry comment', $entry, $user);
$json = $this->entryCommentNoteFactory->create($comment, $this->tagLinkRepository->getTagsOfContent($comment));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($comment);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$announceActivity = $this->announceWrapper->build($magazine, $createActivity);
$announce = $this->activityJsonBuilder->buildActivityJson($announceActivity);
$this->testingApHttpClient->activityObjects[$announce['id']] = $announce;
if (null !== $entryCommentCreateCallback) {
$entryCommentCreateCallback($comment);
}
$this->entitiesToRemoveAfterSetup[] = $announceActivity;
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $comment;
return $announce;
}
/**
* @param callable(Post $entry):void|null $postCreateCallback
*/
protected function createRemotePostInRemoteMagazine(Magazine $magazine, User $user, ?callable $postCreateCallback = null): array
{
$post = $this->createPost('remote post', magazine: $magazine, user: $user);
$json = $this->postNoteFactory->create($post, $this->tagLinkRepository->getTagsOfContent($post));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($post);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$announceActivity = $this->announceWrapper->build($magazine, $createActivity);
$announce = $this->activityJsonBuilder->buildActivityJson($announceActivity);
$this->testingApHttpClient->activityObjects[$announce['id']] = $announce;
if (null !== $postCreateCallback) {
$postCreateCallback($post);
}
$this->entitiesToRemoveAfterSetup[] = $announceActivity;
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $post;
return $announce;
}
/**
* @param callable(PostComment $entry):void|null $postCommentCreateCallback
*/
protected function createRemotePostCommentInRemoteMagazine(Magazine $magazine, User $user, ?callable $postCommentCreateCallback = null): array
{
$posts = array_filter($this->entitiesToRemoveAfterSetup, fn ($item) => $item instanceof Post);
$post = $posts[array_key_first($posts)];
$comment = $this->createPostComment('remote post comment', $post, $user);
$json = $this->postCommentNoteFactory->create($comment, $this->tagLinkRepository->getTagsOfContent($comment));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($comment);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$announceActivity = $this->announceWrapper->build($magazine, $createActivity);
$announce = $this->activityJsonBuilder->buildActivityJson($announceActivity);
$this->testingApHttpClient->activityObjects[$announce['id']] = $announce;
if (null !== $postCommentCreateCallback) {
$postCommentCreateCallback($comment);
}
$this->entitiesToRemoveAfterSetup[] = $announceActivity;
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $comment;
return $announce;
}
/**
* @param callable(Entry $entry):void|null $entryCreateCallback
*/
protected function createRemoteEntryInLocalMagazine(Magazine $magazine, User $user, ?callable $entryCreateCallback = null): array
{
$entry = $this->getEntryByTitle('remote entry in local', magazine: $magazine, user: $user);
$json = $this->pageFactory->create($entry, $this->tagLinkRepository->getTagsOfContent($entry));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($entry);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$create = $this->RewriteTargetFieldsToLocal($magazine, $create);
if (null !== $entryCreateCallback) {
$entryCreateCallback($entry);
}
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $entry;
return $create;
}
/**
* @param callable(EntryComment $entry):void|null $entryCommentCreateCallback
*/
protected function createRemoteEntryCommentInLocalMagazine(Magazine $magazine, User $user, ?callable $entryCommentCreateCallback = null): array
{
$entries = array_filter($this->entitiesToRemoveAfterSetup, fn ($item) => $item instanceof Entry && 'remote entry in local' === $item->title);
$entry = $entries[array_key_first($entries)];
$comment = $this->createEntryComment('remote entry comment', $entry, $user);
$json = $this->entryCommentNoteFactory->create($comment, $this->tagLinkRepository->getTagsOfContent($comment));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($comment);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$create = $this->RewriteTargetFieldsToLocal($magazine, $create);
if (null !== $entryCommentCreateCallback) {
$entryCommentCreateCallback($comment);
}
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $comment;
return $create;
}
/**
* @param callable(Post $entry):void|null $postCreateCallback
*/
protected function createRemotePostInLocalMagazine(Magazine $magazine, User $user, ?callable $postCreateCallback = null): array
{
$post = $this->createPost('remote post in local', magazine: $magazine, user: $user);
$json = $this->postNoteFactory->create($post, $this->tagLinkRepository->getTagsOfContent($post));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($post);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$create = $this->RewriteTargetFieldsToLocal($magazine, $create);
if (null !== $postCreateCallback) {
$postCreateCallback($post);
}
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $post;
return $create;
}
/**
* @param callable(PostComment $entry):void|null $postCommentCreateCallback
*/
protected function createRemotePostCommentInLocalMagazine(Magazine $magazine, User $user, ?callable $postCommentCreateCallback = null): array
{
$posts = array_filter($this->entitiesToRemoveAfterSetup, fn ($item) => $item instanceof Post && 'remote post in local' === $item->body);
$post = $posts[array_key_first($posts)];
$comment = $this->createPostComment('remote post comment in local', $post, $user);
$json = $this->postCommentNoteFactory->create($comment, $this->tagLinkRepository->getTagsOfContent($comment));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($comment);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$create = $this->RewriteTargetFieldsToLocal($magazine, $create);
if (null !== $postCommentCreateCallback) {
$postCommentCreateCallback($comment);
}
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $comment;
return $create;
}
/**
* @param callable(Message $entry):void|null $messageCreateCallback
*/
protected function createRemoteMessage(User $fromRemoteUser, User $toLocalUser, ?callable $messageCreateCallback = null): array
{
$dto = new MessageDto();
$dto->body = 'remote message';
$thread = $this->messageManager->toThread($dto, $fromRemoteUser, $toLocalUser);
$message = $thread->getLastMessage();
$this->entitiesToRemoveAfterSetup[] = $thread;
$this->entitiesToRemoveAfterSetup[] = $message;
$createActivity = $this->createWrapper->build($message);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$correctUserString = "https://$this->prev/u/$toLocalUser->username";
$create['to'] = [$correctUserString];
$create['object']['to'] = [$correctUserString];
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
if (null !== $messageCreateCallback) {
$messageCreateCallback($message);
}
$this->entitiesToRemoveAfterSetup[] = $createActivity;
return $create;
}
/**
* This rewrites the target fields `to` and `audience` to the @see self::$prev domain.
* This is useful when remote actors create activities on local magazines.
*
* @return array the array with rewritten target fields
*/
protected function RewriteTargetFieldsToLocal(Magazine $magazine, array $activityArray): array
{
$magazineAddress = "https://$this->prev/m/$magazine->name";
$to = [
$magazineAddress,
ActivityPubActivityInterface::PUBLIC_URL,
];
if (isset($activityArray['to'])) {
$activityArray['to'] = $to;
}
if (isset($activityArray['audience'])) {
$activityArray['audience'] = $magazineAddress;
}
if (isset($activityArray['object']) && \is_array($activityArray['object'])) {
$activityArray['object'] = $this->RewriteTargetFieldsToLocal($magazine, $activityArray['object']);
}
return $activityArray;
}
protected function assertCountOfSentActivitiesOfType(int $expectedCount, string $type): void
{
$activities = $this->getSentActivitiesOfType($type);
$this->assertCount($expectedCount, $activities);
}
protected function assertOneSentActivityOfType(string $type, ?string $activityId = null, ?string $inboxUrl = null): array
{
$activities = $this->getSentActivitiesOfType($type);
self::assertCount(1, $activities);
if (null !== $activityId) {
self::assertEquals($activityId, $activities[0]['payload']['id']);
}
if (null !== $inboxUrl) {
self::assertEquals($inboxUrl, $activities[0]['inboxUrl']);
}
return $activities[0]['payload'];
}
protected function assertOneSentAnnouncedActivityOfType(string $type, ?string $announcedActivityId = null): void
{
$activities = $this->getSentAnnounceActivitiesOfInnerType($type);
self::assertCount(1, $activities);
if (null !== $announcedActivityId) {
self::assertEquals($announcedActivityId, $activities[0]['payload']['object']['id']);
}
}
protected function assertOneSentAnnouncedActivityOfTypeGetInnerActivity(string $type, ?string $announcedActivityId = null, ?string $announceId = null, ?string $inboxUrl = null): array|string
{
$activities = $this->getSentAnnounceActivitiesOfInnerType($type);
self::assertCount(1, $activities);
if (null !== $announcedActivityId) {
self::assertEquals($announcedActivityId, $activities[0]['payload']['object']['id']);
}
if (null !== $announceId) {
self::assertEquals($announceId, $activities[0]['payload']['id']);
}
if (null !== $inboxUrl) {
self::assertEquals($inboxUrl, $activities[0]['inboxUrl']);
}
return $activities[0]['payload']['object'];
}
/**
* @return array
*/
protected function getSentActivitiesOfType(string $type): array
{
return array_values(array_filter($this->testingApHttpClient->getPostedObjects(), fn (array $item) => $type === $item['payload']['type']));
}
/**
* @return array
*/
protected function getSentAnnounceActivitiesOfInnerType(string $type): array
{
return array_values(array_filter($this->testingApHttpClient->getPostedObjects(), fn (array $item) => 'Announce' === $item['payload']['type'] && $type === $item['payload']['object']['type']));
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/AcceptHandlerTest.php
================================================
remoteUser->apManuallyApprovesFollowers = true;
$this->userManager->follow($this->localUser, $this->remoteUser);
}
public function setUpLocalEntities(): void
{
$followActivity = $this->followWrapper->build($this->localUser, $this->remoteUser);
$this->followRemoteUser = $this->activityJsonBuilder->buildActivityJson($followActivity);
$this->testingApHttpClient->activityObjects[$this->followRemoteUser['id']] = $this->followRemoteUser;
$this->entitiesToRemoveAfterSetup[] = $followActivity;
$this->magazineManager->subscribe($this->remoteMagazine, $this->localUser);
$followActivity = $this->followWrapper->build($this->localUser, $this->remoteMagazine);
$this->followRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($followActivity);
$this->testingApHttpClient->activityObjects[$this->followRemoteMagazine['id']] = $this->followRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $followActivity;
}
public function setUpRemoteEntities(): void
{
}
public function setUpLateRemoteEntities(): void
{
$acceptActivity = $this->followResponseWrapper->build($this->remoteUser, $this->followRemoteUser);
$this->acceptFollowRemoteUser = $this->activityJsonBuilder->buildActivityJson($acceptActivity);
$this->testingApHttpClient->activityObjects[$this->acceptFollowRemoteUser['id']] = $this->acceptFollowRemoteUser;
$this->entitiesToRemoveAfterSetup[] = $acceptActivity;
$acceptActivity = $this->followResponseWrapper->build($this->remoteMagazine, $this->followRemoteMagazine);
$this->acceptFollowRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($acceptActivity);
$this->testingApHttpClient->activityObjects[$this->acceptFollowRemoteMagazine['id']] = $this->acceptFollowRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $acceptActivity;
}
public function testAcceptFollowMagazine(): void
{
// we do not have manual follower approving for magazines implemented
$this->bus->dispatch(new ActivityMessage(json_encode($this->acceptFollowRemoteMagazine)));
}
public function testAcceptFollowUser(): void
{
self::assertTrue($this->remoteUser->apManuallyApprovesFollowers);
$request = $this->userFollowRequestRepository->findOneby(['follower' => $this->localUser, 'following' => $this->remoteUser]);
self::assertNotNull($request);
$this->bus->dispatch(new ActivityMessage(json_encode($this->acceptFollowRemoteUser)));
$request = $this->userFollowRequestRepository->findOneby(['follower' => $this->localUser, 'following' => $this->remoteUser]);
self::assertNull($request);
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/AddHandlerTest.php
================================================
magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->remoteUser, $this->remoteMagazine->getOwner()));
$this->magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteUser, $this->localUser));
}
public function testAddModeratorInRemoteMagazine(): void
{
self::assertFalse($this->remoteMagazine->userIsModerator($this->remoteSubscriber));
$this->bus->dispatch(new ActivityMessage(json_encode($this->addModeratorRemoteMagazine)));
self::assertTrue($this->remoteMagazine->userIsModerator($this->remoteSubscriber));
}
public function testAddModeratorLocalMagazine(): void
{
self::assertFalse($this->localMagazine->userIsModerator($this->remoteSubscriber));
$this->bus->dispatch(new ActivityMessage(json_encode($this->addModeratorLocalMagazine)));
self::assertTrue($this->localMagazine->userIsModerator($this->remoteSubscriber));
$this->assertAddSentToSubscriber($this->addModeratorLocalMagazine);
}
public function testAddPinnedEntryInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInRemoteMagazine['object']['object']['id']]);
self::assertNotNull($entry);
self::assertFalse($entry->sticky);
$this->bus->dispatch(new ActivityMessage(json_encode($this->addPinnedEntryRemoteMagazine)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->sticky);
}
public function testAddPinnedEntryLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInLocalMagazine['object']['id']]);
self::assertNotNull($entry);
self::assertFalse($entry->sticky);
$this->bus->dispatch(new ActivityMessage(json_encode($this->addPinnedEntryLocalMagazine)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->sticky);
$this->assertAddSentToSubscriber($this->addPinnedEntryLocalMagazine);
}
public function setUpRemoteEntities(): void
{
$this->buildAddModeratorInRemoteMagazine();
$this->buildAddModeratorInLocalMagazine();
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildAddPinnedPostInRemoteMagazine($entry));
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildAddPinnedPostInLocalMagazine($entry));
}
private function buildAddModeratorInRemoteMagazine(): void
{
$addActivity = $this->addRemoveFactory->buildAddModerator($this->remoteUser, $this->remoteSubscriber, $this->remoteMagazine);
$this->addModeratorRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($addActivity);
$this->addModeratorRemoteMagazine['object'] = 'https://remote.sub.mbin/u/remoteSubscriber';
$this->testingApHttpClient->activityObjects[$this->addModeratorRemoteMagazine['id']] = $this->addModeratorRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $addActivity;
}
private function buildAddModeratorInLocalMagazine(): void
{
$addActivity = $this->addRemoveFactory->buildAddModerator($this->remoteUser, $this->remoteSubscriber, $this->localMagazine);
$this->addModeratorLocalMagazine = $this->activityJsonBuilder->buildActivityJson($addActivity);
$this->addModeratorLocalMagazine['target'] = 'https://kbin.test/m/magazine/moderators';
$this->addModeratorLocalMagazine['object'] = 'https://remote.sub.mbin/u/remoteSubscriber';
$this->testingApHttpClient->activityObjects[$this->addModeratorLocalMagazine['id']] = $this->addModeratorLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $addActivity;
}
private function buildAddPinnedPostInRemoteMagazine(Entry $entry): void
{
$addActivity = $this->addRemoveFactory->buildAddPinnedPost($this->remoteUser, $entry);
$this->addPinnedEntryRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($addActivity);
$this->testingApHttpClient->activityObjects[$this->addPinnedEntryRemoteMagazine['id']] = $this->addPinnedEntryRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $addActivity;
}
private function buildAddPinnedPostInLocalMagazine(Entry $entry): void
{
$addActivity = $this->addRemoveFactory->buildAddPinnedPost($this->remoteUser, $entry);
$this->addPinnedEntryLocalMagazine = $this->activityJsonBuilder->buildActivityJson($addActivity);
$this->addPinnedEntryLocalMagazine['target'] = 'https://kbin.test/m/magazine/pinned';
$this->testingApHttpClient->activityObjects[$this->addPinnedEntryLocalMagazine['id']] = $this->addPinnedEntryLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $addActivity;
}
private function assertAddSentToSubscriber(array $originalPayload): void
{
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedAddAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Add' === $arr['payload']['object']['type']);
$postedAddAnnounce = $postedAddAnnounces[array_key_first($postedAddAnnounces)];
// the id of the 'Add' activity should be wrapped in an 'Announce' activity
self::assertEquals($originalPayload['id'], $postedAddAnnounce['payload']['object']['id']);
self::assertEquals($originalPayload['object'], $postedAddAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedAddAnnounce['inboxUrl']);
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/BlockHandlerTest.php
================================================
magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteSubscriber, $this->localUser));
$this->remoteAdmin = $this->activityPubManager->findActorOrCreate("@remoteAdmin@$this->remoteDomain");
$this->magazineManager->subscribe($this->localMagazine, $this->remoteUser);
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$user = $this->getUserByUsername('remoteAdmin', addImage: false);
$this->registerActor($user, $this->remoteDomain, true);
$this->remoteAdmin = $user;
}
public function setupLocalActors(): void
{
$this->localSubscriber = $this->getUserByUsername('localSubscriber', addImage: false);
parent::setupLocalActors();
}
public function setUpRemoteEntities(): void
{
$this->buildBlockLocalUserLocalMagazine();
$this->buildBlockLocalUserRemoteMagazine();
$this->buildBlockRemoteUserLocalMagazine();
$this->buildBlockRemoteUserRemoteMagazine();
$this->buildInstanceBanRemoteUser();
}
public function testBlockLocalUserLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->blockLocalUserLocalMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->localMagazine, 'user' => $this->localSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->localMagazine);
self::assertTrue($this->localMagazine->isBanned($this->localSubscriber));
// should not be sent to source instance, only to subscriber instance
$announcedBlock = $this->assertOneSentAnnouncedActivityOfTypeGetInnerActivity('Block', announcedActivityId: $this->blockLocalUserLocalMagazine['id'], inboxUrl: $this->remoteUser->apInboxUrl);
self::assertEquals($this->blockLocalUserLocalMagazine['object'], $announcedBlock['object']);
}
#[Depends('testBlockLocalUserLocalMagazine')]
public function testUndoBlockLocalUserLocalMagazine(): void
{
$this->testBlockLocalUserLocalMagazine();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoBlockLocalUserLocalMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->localMagazine, 'user' => $this->localSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->localMagazine);
self::assertFalse($this->localMagazine->isBanned($this->localSubscriber));
// should not be sent to source instance, only to subscriber instance
$announcedUndo = $this->assertOneSentAnnouncedActivityOfTypeGetInnerActivity('Undo', announcedActivityId: $this->undoBlockLocalUserLocalMagazine['id'], inboxUrl: $this->remoteUser->apInboxUrl);
self::assertEquals($this->undoBlockLocalUserLocalMagazine['object']['object'], $announcedUndo['object']['object']);
self::assertEquals($this->undoBlockLocalUserLocalMagazine['object']['id'], $announcedUndo['object']['id']);
}
public function testBlockRemoteUserLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->blockRemoteUserLocalMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->localMagazine, 'user' => $this->remoteUser]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->localMagazine);
self::assertTrue($this->localMagazine->isBanned($this->remoteUser));
// should not be sent to source instance, only to subscriber instance
$blockActivity = $this->assertOneSentAnnouncedActivityOfTypeGetInnerActivity('Block', announcedActivityId: $this->blockRemoteUserLocalMagazine['id'], inboxUrl: $this->remoteUser->apInboxUrl);
self::assertEquals($this->blockRemoteUserLocalMagazine['object'], $blockActivity['object']);
}
#[Depends('testBlockRemoteUserLocalMagazine')]
public function testUndoBlockRemoteUserLocalMagazine(): void
{
$this->testBlockRemoteUserLocalMagazine();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoBlockRemoteUserLocalMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->localMagazine, 'user' => $this->remoteUser]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->localMagazine);
self::assertFalse($this->localMagazine->isBanned($this->remoteUser));
// should not be sent to source instance, only to subscriber instance
$announcedUndo = $this->assertOneSentAnnouncedActivityOfTypeGetInnerActivity('Undo', announcedActivityId: $this->undoBlockRemoteUserLocalMagazine['id'], inboxUrl: $this->remoteUser->apInboxUrl);
self::assertEquals($this->undoBlockRemoteUserLocalMagazine['object']['id'], $announcedUndo['object']['id']);
self::assertEquals($this->undoBlockRemoteUserLocalMagazine['object']['object'], $announcedUndo['object']['object']);
}
public function testBlockLocalUserRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->blockLocalUserRemoteMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->remoteMagazine, 'user' => $this->localSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->remoteMagazine);
self::assertTrue($this->remoteMagazine->isBanned($this->localSubscriber));
}
#[Depends('testBlockLocalUserRemoteMagazine')]
public function testUndoBlockLocalUserRemoteMagazine(): void
{
$this->testBlockLocalUserRemoteMagazine();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoBlockLocalUserRemoteMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->remoteMagazine, 'user' => $this->localSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->remoteMagazine);
self::assertFalse($this->remoteMagazine->isBanned($this->localSubscriber));
}
public function testBlockRemoteUserRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->blockRemoteUserRemoteMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->remoteMagazine, 'user' => $this->remoteSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->remoteMagazine);
self::assertTrue($this->remoteMagazine->isBanned($this->remoteSubscriber));
}
#[Depends('testBlockRemoteUserRemoteMagazine')]
public function testUndoBlockRemoteUserRemoteMagazine(): void
{
$this->testBlockRemoteUserRemoteMagazine();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoBlockRemoteUserRemoteMagazine)));
$ban = $this->magazineBanRepository->findOneBy(['magazine' => $this->remoteMagazine, 'user' => $this->remoteSubscriber]);
self::assertNotNull($ban);
$this->entityManager->refresh($this->remoteMagazine);
self::assertFalse($this->remoteMagazine->isBanned($this->remoteSubscriber));
}
public function testInstanceBanRemoteUser(): void
{
$username = "@remoteUser@$this->remoteDomain";
$remoteUser = $this->userRepository->findOneByUsername($username);
self::assertFalse($remoteUser->isBanned);
$this->bus->dispatch(new ActivityMessage(json_encode($this->instanceBanRemoteUser)));
$this->entityManager->refresh($remoteUser);
self::assertTrue($remoteUser->isBanned);
self::assertEquals('testing', $remoteUser->banReason);
}
#[Depends('testInstanceBanRemoteUser')]
public function testUndoInstanceBanRemoteUser(): void
{
$this->testInstanceBanRemoteUser();
$username = "@remoteUser@$this->remoteDomain";
$remoteUser = $this->userRepository->findOneByUsername($username);
self::assertTrue($remoteUser->isBanned);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoInstanceBanRemoteUser)));
$this->entityManager->refresh($remoteUser);
self::assertFalse($remoteUser->isBanned);
}
private function buildBlockLocalUserLocalMagazine(): void
{
$ban = $this->magazineManager->ban($this->localMagazine, $this->localSubscriber, $this->remoteSubscriber, MagazineBanDto::create('testing'));
$activity = $this->blockFactory->createActivityFromMagazineBan($ban);
$this->blockLocalUserLocalMagazine = $this->activityJsonBuilder->buildActivityJson($activity);
$this->blockLocalUserLocalMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->blockLocalUserLocalMagazine['actor']);
$this->blockLocalUserLocalMagazine['object'] = str_replace($this->remoteDomain, $this->localDomain, $this->blockLocalUserLocalMagazine['object']);
$this->blockLocalUserLocalMagazine['target'] = str_replace($this->remoteDomain, $this->localDomain, $this->blockLocalUserLocalMagazine['target']);
$undoActivity = $this->undoWrapper->build($activity);
$this->undoBlockLocalUserLocalMagazine = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->undoBlockLocalUserLocalMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->undoBlockLocalUserLocalMagazine['actor']);
$this->undoBlockLocalUserLocalMagazine['object'] = $this->blockLocalUserLocalMagazine;
$this->testingApHttpClient->activityObjects[$this->blockLocalUserLocalMagazine['id']] = $this->blockLocalUserLocalMagazine;
$this->testingApHttpClient->activityObjects[$this->undoBlockLocalUserLocalMagazine['id']] = $this->undoBlockLocalUserLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $activity;
}
private function buildBlockRemoteUserLocalMagazine(): void
{
$ban = $this->magazineManager->ban($this->localMagazine, $this->remoteUser, $this->remoteSubscriber, MagazineBanDto::create('testing'));
$activity = $this->blockFactory->createActivityFromMagazineBan($ban);
$this->blockRemoteUserLocalMagazine = $this->activityJsonBuilder->buildActivityJson($activity);
$this->blockRemoteUserLocalMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->blockRemoteUserLocalMagazine['actor']);
$this->blockRemoteUserLocalMagazine['target'] = str_replace($this->remoteDomain, $this->localDomain, $this->blockRemoteUserLocalMagazine['target']);
$undoActivity = $this->undoWrapper->build($activity);
$this->undoBlockRemoteUserLocalMagazine = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->undoBlockRemoteUserLocalMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->undoBlockRemoteUserLocalMagazine['actor']);
$this->undoBlockRemoteUserLocalMagazine['object'] = $this->blockRemoteUserLocalMagazine;
$this->testingApHttpClient->activityObjects[$this->blockRemoteUserLocalMagazine['id']] = $this->blockRemoteUserLocalMagazine;
$this->testingApHttpClient->activityObjects[$this->undoBlockRemoteUserLocalMagazine['id']] = $this->undoBlockRemoteUserLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $activity;
}
private function buildBlockLocalUserRemoteMagazine(): void
{
$ban = $this->magazineManager->ban($this->remoteMagazine, $this->localSubscriber, $this->remoteSubscriber, MagazineBanDto::create('testing'));
$activity = $this->blockFactory->createActivityFromMagazineBan($ban);
$this->blockLocalUserRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($activity);
$this->blockLocalUserRemoteMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->blockLocalUserRemoteMagazine['actor']);
$this->blockLocalUserRemoteMagazine['object'] = str_replace($this->remoteDomain, $this->localDomain, $this->blockLocalUserRemoteMagazine['object']);
$undoActivity = $this->undoWrapper->build($activity);
$this->undoBlockLocalUserRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->undoBlockLocalUserRemoteMagazine['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->undoBlockLocalUserRemoteMagazine['actor']);
$this->undoBlockLocalUserRemoteMagazine['object'] = $this->blockLocalUserRemoteMagazine;
$this->testingApHttpClient->activityObjects[$this->blockLocalUserRemoteMagazine['id']] = $this->blockLocalUserRemoteMagazine;
$this->testingApHttpClient->activityObjects[$this->undoBlockLocalUserRemoteMagazine['id']] = $this->undoBlockLocalUserRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $activity;
}
private function buildBlockRemoteUserRemoteMagazine(): void
{
$ban = $this->magazineManager->ban($this->remoteMagazine, $this->remoteSubscriber, $this->remoteUser, MagazineBanDto::create('testing'));
$activity = $this->blockFactory->createActivityFromMagazineBan($ban);
$this->blockRemoteUserRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($activity);
$this->blockRemoteUserRemoteMagazine['object'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $this->blockRemoteUserRemoteMagazine['object']);
$undoActivity = $this->undoWrapper->build($activity);
$this->undoBlockRemoteUserRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->undoBlockRemoteUserRemoteMagazine['object'] = $this->blockRemoteUserRemoteMagazine;
$this->testingApHttpClient->activityObjects[$this->blockRemoteUserRemoteMagazine['id']] = $this->blockRemoteUserRemoteMagazine;
$this->testingApHttpClient->activityObjects[$this->undoBlockRemoteUserRemoteMagazine['id']] = $this->undoBlockRemoteUserRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $activity;
}
private function buildInstanceBanRemoteUser(): void
{
$this->remoteUser->banReason = 'testing';
$activity = $this->blockFactory->createActivityFromInstanceBan($this->remoteUser, $this->remoteAdmin);
$this->instanceBanRemoteUser = $this->activityJsonBuilder->buildActivityJson($activity);
$this->testingApHttpClient->activityObjects[$this->instanceBanRemoteUser['id']] = $this->instanceBanRemoteUser;
$this->entitiesToRemoveAfterSetup[] = $activity;
$activity = $this->undoWrapper->build($activity);
$this->undoInstanceBanRemoteUser = $this->activityJsonBuilder->buildActivityJson($activity);
$this->testingApHttpClient->activityObjects[$this->undoInstanceBanRemoteUser['id']] = $this->undoInstanceBanRemoteUser;
$this->entitiesToRemoveAfterSetup[] = $activity;
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/CreateHandlerTest.php
================================================
announceEntry = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser);
$this->announceEntryComment = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser);
$this->announcePost = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remoteUser);
$this->announcePostComment = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser);
$this->createEntry = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser);
$this->createEntryWithUrlAndImage = $this->createRemoteEntryWithUrlAndImageInLocalMagazine($this->localMagazine, $this->remoteUser);
$this->createEntryComment = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remoteUser);
$this->createPost = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser);
$this->createPostComment = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remoteUser);
$this->createMessage = $this->createRemoteMessage($this->remoteUser, $this->localUser);
$this->setupMastodonPost();
$this->setupMastodonPostWithoutTagArray();
}
public function setUpLocalEntities(): void
{
$this->setupRemoteActor();
}
public function testCreateAnnouncedEntry(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertNotNull($entry);
}
#[Depends('testCreateAnnouncedEntry')]
public function testCreateAnnouncedEntryComment(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertNotNull($entryComment);
}
#[Depends('testCreateAnnouncedEntryComment')]
public function testCannotCreateAnnouncedEntryCommentInLockedEntry(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertNotNull($entry);
$entry->isLocked = true;
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
// the comment should not be created and therefore be null
self::assertNull($entryComment);
}
public function testCreateAnnouncedPost(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertNotNull($post);
}
#[Depends('testCreateAnnouncedPost')]
public function testCreateAnnouncedPostComment(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertNotNull($postComment);
}
#[Depends('testCreateAnnouncedPostComment')]
public function testCannotCreateAnnouncedPostCommentInLockedPost(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertNotNull($post);
$post->isLocked = true;
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
// the comment should not be created and therefore be null
self::assertNull($postComment);
}
public function testCreateEntry(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertNotNull($entry);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
// the id of the 'Create' activity should be wrapped in a 'Announce' activity
self::assertEquals($this->createEntry['id'], $postedObjects[0]['payload']['object']['id']);
self::assertEquals($this->createEntry['object']['id'], $postedObjects[0]['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedObjects[0]['inboxUrl']);
}
public function testCreateEntryWithUrlAndImage(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryWithUrlAndImage)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntryWithUrlAndImage['object']['id']]);
self::assertNotNull($entry);
self::assertNotNull($entry->image);
self::assertNotNull($entry->url);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
// the id of the 'Create' activity should be wrapped in a 'Announce' activity
self::assertEquals($this->createEntryWithUrlAndImage['id'], $postedObjects[0]['payload']['object']['id']);
self::assertEquals($this->createEntryWithUrlAndImage['object']['id'], $postedObjects[0]['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedObjects[0]['inboxUrl']);
}
#[Depends('testCreateEntry')]
public function testCreateEntryComment(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertCount(2, $postedObjects);
// the id of the 'Create' activity should be wrapped in a 'Announce' activity
self::assertEquals($this->createEntryComment['id'], $postedObjects[1]['payload']['object']['id']);
self::assertEquals($this->createEntryComment['object']['id'], $postedObjects[1]['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedObjects[1]['inboxUrl']);
}
public function testCreatePost(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
// the id of the 'Create' activity should be wrapped in a 'Announce' activity
self::assertEquals($this->createPost['id'], $postedObjects[0]['payload']['object']['id']);
self::assertEquals($this->createPost['object']['id'], $postedObjects[0]['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedObjects[0]['inboxUrl']);
}
#[Depends('testCreatePost')]
public function testCreatePostComment(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertTrue($this->localMagazine->isSubscribed($this->remoteSubscriber));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertCount(2, $postedObjects);
// the id of the 'Create' activity should be wrapped in a 'Announce' activity
self::assertEquals($this->createPostComment['id'], $postedObjects[1]['payload']['object']['id']);
self::assertEquals($this->createPostComment['object']['id'], $postedObjects[1]['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedObjects[1]['inboxUrl']);
}
public function testCreateMessage(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMessage)));
$message = $this->messageRepository->findOneBy(['apId' => $this->createMessage['object']['id']]);
self::assertNotNull($message);
}
public function testCreateMessageFollowersOnlyFails(): void
{
$this->localUser->directMessageSetting = EDirectMessageSettings::FollowersOnly->value;
self::expectException(HandlerFailedException::class);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMessage)));
}
public function testCreateMessageFollowersOnly(): void
{
$this->localUser->directMessageSetting = EDirectMessageSettings::FollowersOnly->value;
$this->userManager->follow($this->remoteUser, $this->localUser);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMessage)));
$message = $this->messageRepository->findOneBy(['apId' => $this->createMessage['object']['id']]);
self::assertNotNull($message);
}
public function testCreateMessageNobodyFails(): void
{
$this->localUser->directMessageSetting = EDirectMessageSettings::Nobody->value;
$this->userManager->follow($this->remoteUser, $this->localUser);
self::expectException(HandlerFailedException::class);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMessage)));
}
public function testMastodonMentionInPost(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMastodonPostWithMention)));
$post = $this->postRepository->findOneBy(['apId' => $this->createMastodonPostWithMention['object']['id']]);
self::assertNotNull($post);
$mentions = $this->mentionManager->extract($post->body);
self::assertCount(3, $mentions);
self::assertEquals('@someOtherUser@some.instance.tld', $mentions[0]);
self::assertEquals('@someUser@some.instance.tld', $mentions[1]);
self::assertEquals('@someMagazine@some.instance.tld', $mentions[2]);
}
public function testMastodonMentionInPostWithoutTagArray(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createMastodonPostWithMentionWithoutTagArray)));
$post = $this->postRepository->findOneBy(['apId' => $this->createMastodonPostWithMentionWithoutTagArray['object']['id']]);
self::assertNotNull($post);
$mentions = $this->mentionManager->extract($post->body);
self::assertCount(1, $mentions);
self::assertEquals('@remoteUser@remote.mbin', $mentions[0]);
}
private function setupRemoteActor(): void
{
$domain = 'some.instance.tld';
$this->switchToRemoteDomain($domain);
$user = $this->getUserByUsername('someOtherUser', addImage: false, email: 'user@some.tld');
$this->registerActor($user, $domain, true);
$user = $this->getUserByUsername('someUser', addImage: false, email: 'user2@some.tld');
$this->registerActor($user, $domain, true);
$magazine = $this->getMagazineByName('someMagazine', user: $user);
$this->registerActor($magazine, $domain, true);
$this->switchToLocalDomain();
}
private function setupMastodonPost(): void
{
$this->createMastodonPostWithMention = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser);
unset($this->createMastodonPostWithMention['object']['source']);
// this is what it would look like if a user created a post in Mastodon with just a single mention and nothing else
$text = '@someOtherUser @someUser @someMagazine
';
$this->createMastodonPostWithMention['object']['contentMap']['en'] = $text;
$this->createMastodonPostWithMention['object']['content'] = $text;
$this->createMastodonPostWithMention['object']['tag'] = [
[
'type' => 'Mention',
'href' => 'https://some.instance.tld/u/someOtherUser',
'name' => '@someOtherUser',
],
[
'type' => 'Mention',
'href' => 'https://some.instance.tld/u/someUser',
'name' => '@someUser',
],
[
'type' => 'Mention',
'href' => 'https://some.instance.tld/m/someMagazine',
'name' => '@someMagazine',
],
];
}
private function setupMastodonPostWithoutTagArray(): void
{
$this->createMastodonPostWithMentionWithoutTagArray = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser);
unset($this->createMastodonPostWithMentionWithoutTagArray['object']['source']);
// this is what it would look like if a user created a post in Mastodon with just a single mention and nothing else
$text = '@remoteUser ';
$this->createMastodonPostWithMentionWithoutTagArray['object']['contentMap']['en'] = $text;
$this->createMastodonPostWithMentionWithoutTagArray['object']['content'] = $text;
}
private function createRemoteEntryWithUrlAndImageInLocalMagazine(Magazine $magazine, User $user): array
{
$entry = $this->getEntryByTitle('remote entry with URL and image in local', url: 'https://joinmbin.org', magazine: $magazine, user: $user, image: $this->getKibbyImageDto());
$json = $this->pageFactory->create($entry, $this->tagLinkRepository->getTagsOfContent($entry));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$createActivity = $this->createWrapper->build($entry);
$create = $this->activityJsonBuilder->buildActivityJson($createActivity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$create = $this->RewriteTargetFieldsToLocal($magazine, $create);
$this->entitiesToRemoveAfterSetup[] = $createActivity;
$this->entitiesToRemoveAfterSetup[] = $entry;
return $create;
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/DeleteHandlerTest.php
================================================
createLocalEntryAndCreateDeleteActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$entry = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteLocalEntryInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalEntryAndCreateDeleteActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$entry = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteRemoteEntryInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entryApId = $this->createRemoteEntryInLocalMagazine['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemoteEntryByRemoteModeratorInLocalMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertTrue($entry->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $this->deleteRemoteEntryByRemoteModeratorInLocalMagazine['id']);
}
public function testDeleteRemoteEntryInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entryApId = $this->createRemoteEntryInRemoteMagazine['object']['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemoteEntryByRemoteModeratorInRemoteMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertTrue($entry->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$deleteActivities = $this->activityRepository->findBy(['type' => 'Delete']);
self::assertEmpty($deleteActivities);
}
public function testDeleteLocalEntryCommentInLocalMagazineByRemoteModerator(): void
{
$obj = $this->createLocalEntryCommentAndCreateDeleteActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$entryComment = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entryComment);
self::assertTrue($entryComment->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteLocalEntryCommentInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalEntryCommentAndCreateDeleteActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$entryComment = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entryComment);
self::assertTrue($entryComment->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteRemoteEntryCommentInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entryApId = $this->createRemoteEntryInLocalMagazine['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryCommentInLocalMagazine)));
$entryCommentApId = $this->createRemoteEntryCommentInLocalMagazine['object']['id'];
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertNotNull($entryComment);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemoteEntryCommentByRemoteModeratorInLocalMagazine)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertTrue($entryComment->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $this->deleteRemoteEntryCommentByRemoteModeratorInLocalMagazine['id']);
}
public function testDeleteRemoteEntryCommentInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entryApId = $this->createRemoteEntryInRemoteMagazine['object']['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryCommentInRemoteMagazine)));
$entryCommentApId = $this->createRemoteEntryCommentInRemoteMagazine['object']['object']['id'];
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertNotNull($entryComment);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemoteEntryCommentByRemoteModeratorInRemoteMagazine)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertTrue($entryComment->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$deleteActivities = $this->activityRepository->findBy(['type' => 'Delete']);
self::assertEmpty($deleteActivities);
}
public function testDeleteLocalPostInLocalMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostAndCreateDeleteActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$post = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($post);
self::assertTrue($post->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteLocalPostInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostAndCreateDeleteActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$post = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($post);
self::assertTrue($post->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteRemotePostInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$postApId = $this->createRemotePostInLocalMagazine['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemotePostByRemoteModeratorInLocalMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertTrue($post->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $this->deleteRemotePostByRemoteModeratorInLocalMagazine['id']);
}
public function testDeleteRemotePostInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$postApId = $this->createRemotePostInRemoteMagazine['object']['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemotePostByRemoteModeratorInRemoteMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertTrue($post->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$deleteActivities = $this->activityRepository->findBy(['type' => 'Delete']);
self::assertEmpty($deleteActivities);
}
public function testDeleteLocalPostCommentInLocalMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostCommentAndCreateDeleteActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$PostComment = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($PostComment);
self::assertTrue($PostComment->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteLocalPostCommentInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostCommentAndCreateDeleteActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$postComment = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($postComment);
self::assertTrue($postComment->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertOneSentAnnouncedActivityOfType('Delete', $activity['id']);
}
public function testDeleteRemotePostCommentInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$postApId = $this->createRemotePostInLocalMagazine['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostCommentInLocalMagazine)));
$postCommentApId = $this->createRemotePostCommentInLocalMagazine['object']['id'];
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertNotNull($postComment);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemotePostCommentByRemoteModeratorInLocalMagazine)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertTrue($postComment->isTrashed());
$this->assertOneSentAnnouncedActivityOfType('Delete', $this->deleteRemotePostCommentByRemoteModeratorInLocalMagazine['id']);
}
public function testDeleteRemotePostCommentInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$postApId = $this->createRemotePostInRemoteMagazine['object']['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostCommentInRemoteMagazine)));
$postCommentApId = $this->createRemotePostCommentInRemoteMagazine['object']['object']['id'];
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertNotNull($postComment);
$this->bus->dispatch(new ActivityMessage(json_encode($this->deleteRemotePostCommentByRemoteModeratorInRemoteMagazine)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertTrue($postComment->isTrashed());
$this->assertCountOfSentActivitiesOfType(0, 'Delete');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$deleteActivities = $this->activityRepository->findBy(['type' => 'Delete']);
self::assertEmpty($deleteActivities);
}
public function setUp(): void
{
parent::setUp();
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->remoteUser));
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->localUser));
$this->magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteUser, $this->localMagazine->getOwner()));
$this->magazineManager->subscribe($this->remoteMagazine, $this->remoteSubscriber);
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$username = 'remotePoster';
$domain = $this->remoteDomain;
$this->remotePoster = $this->getUserByUsername($username, addImage: false);
$this->registerActor($this->remotePoster, $domain, true);
}
public function setUpRemoteEntities(): void
{
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($entry) => $this->createDeletesFromRemoteEntryInRemoteMagazine($entry));
$this->createRemoteEntryCommentInRemoteMagazine = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($entryComment) => $this->createDeletesFromRemoteEntryCommentInRemoteMagazine($entryComment));
$this->createRemotePostInRemoteMagazine = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($post) => $this->createDeletesFromRemotePostInRemoteMagazine($post));
$this->createRemotePostCommentInRemoteMagazine = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($comment) => $this->createDeletesFromRemotePostCommentInRemoteMagazine($comment));
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($entry) => $this->createDeletesFromRemoteEntryInLocalMagazine($entry));
$this->createRemoteEntryCommentInLocalMagazine = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($entryComment) => $this->createDeletesFromRemoteEntryCommentInLocalMagazine($entryComment));
$this->createRemotePostInLocalMagazine = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($post) => $this->createDeletesFromRemotePostInLocalMagazine($post));
$this->createRemotePostCommentInLocalMagazine = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($comment) => $this->createDeletesFromRemotePostCommentInLocalMagazine($comment));
}
private function createDeletesFromRemoteEntryInRemoteMagazine(Entry $createdEntry): void
{
$this->deleteRemoteEntryByRemoteModeratorInRemoteMagazine = $this->createDeleteForContent($createdEntry);
}
private function createDeletesFromRemoteEntryInLocalMagazine(Entry $createdEntry): void
{
$this->deleteRemoteEntryByRemoteModeratorInLocalMagazine = $this->createDeleteForContent($createdEntry);
}
private function createDeletesFromRemoteEntryCommentInRemoteMagazine(EntryComment $comment): void
{
$this->deleteRemoteEntryCommentByRemoteModeratorInRemoteMagazine = $this->createDeleteForContent($comment);
}
private function createDeletesFromRemoteEntryCommentInLocalMagazine(EntryComment $comment): void
{
$this->deleteRemoteEntryCommentByRemoteModeratorInLocalMagazine = $this->createDeleteForContent($comment);
}
private function createDeletesFromRemotePostInRemoteMagazine(Post $post): void
{
$this->deleteRemotePostByRemoteModeratorInRemoteMagazine = $this->createDeleteForContent($post);
}
private function createDeletesFromRemotePostInLocalMagazine(Post $ost): void
{
$this->deleteRemotePostByRemoteModeratorInLocalMagazine = $this->createDeleteForContent($ost);
}
private function createDeletesFromRemotePostCommentInRemoteMagazine(PostComment $comment): void
{
$this->deleteRemotePostCommentByRemoteModeratorInRemoteMagazine = $this->createDeleteForContent($comment);
}
private function createDeletesFromRemotePostCommentInLocalMagazine(PostComment $comment): void
{
$this->deleteRemotePostCommentByRemoteModeratorInLocalMagazine = $this->createDeleteForContent($comment);
}
private function createDeleteForContent(Entry|EntryComment|Post|PostComment $content): array
{
$activity = $this->deleteWrapper->build($content, $this->remoteUser);
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$json['summary'] = ' ';
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$this->entitiesToRemoveAfterSetup[] = $activity;
return $json;
}
/**
* @return array{entry:Entry, activity: array}
*/
private function createLocalEntryAndCreateDeleteActivity(Magazine $magazine, User $author, User $deletingUser): array
{
$entry = $this->getEntryByTitle('localEntry', magazine: $magazine, user: $author);
$entryJson = $this->pageFactory->create($entry, [], false);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->deleteWrapper->build($entry, $deletingUser);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $entryJson;
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
return [
'activity' => $activityJson,
'content' => $entry,
];
}
/**
* @return array{content:EntryComment, activity: array}
*/
private function createLocalEntryCommentAndCreateDeleteActivity(Magazine $magazine, User $author, User $deletingUser): array
{
$parent = $this->getEntryByTitle('localEntry', magazine: $magazine, user: $author);
$comment = $this->createEntryComment('localEntryComment', entry: $parent, user: $author);
$commentJson = $this->entryCommentNoteFactory->create($comment, []);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->deleteWrapper->build($comment, $deletingUser);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $commentJson;
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
return [
'activity' => $activityJson,
'content' => $comment,
];
}
/**
* @return array{content:EntryComment, activity: array}
*/
private function createLocalPostAndCreateDeleteActivity(Magazine $magazine, User $author, User $deletingUser): array
{
$post = $this->createPost('localPost', magazine: $magazine, user: $author);
$postJson = $this->postNoteFactory->create($post, []);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->deleteWrapper->build($post, $deletingUser);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $postJson;
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
return [
'activity' => $activityJson,
'content' => $post,
];
}
/**
* @return array{content:EntryComment, activity: array}
*/
private function createLocalPostCommentAndCreateDeleteActivity(Magazine $magazine, User $author, User $deletingUser): array
{
$parent = $this->createPost('localPost', magazine: $magazine, user: $author);
$postComment = $this->createPostComment('localPost', post: $parent, user: $author);
$commentJson = $this->postCommentNoteFactory->create($postComment, []);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->deleteWrapper->build($postComment, $deletingUser);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $commentJson;
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
return [
'activity' => $activityJson,
'content' => $postComment,
];
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/DislikeHandlerTest.php
================================================
bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertSame(0, $entry->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeAnnounceEntry)));
$this->entityManager->refresh($entry);
self::assertNotNull($entry);
self::assertSame(1, $entry->countDownVotes());
}
#[Depends('testDislikeRemoteEntryInRemoteMagazine')]
public function testUndoDislikeRemoteEntryInRemoteMagazine(): void
{
$this->testDislikeRemoteEntryInRemoteMagazine();
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertSame(1, $entry->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeAnnounceEntry)));
$this->entityManager->refresh($entry);
self::assertNotNull($entry);
self::assertSame(0, $entry->countDownVotes());
}
public function testDislikeRemoteEntryCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$comment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertSame(0, $comment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeAnnounceEntryComment)));
$this->entityManager->refresh($comment);
self::assertNotNull($comment);
self::assertSame(1, $comment->countDownVotes());
}
#[Depends('testDislikeRemoteEntryCommentInRemoteMagazine')]
public function testUndoLikeRemoteEntryCommentInRemoteMagazine(): void
{
$this->testDislikeRemoteEntryCommentInRemoteMagazine();
$comment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertSame(1, $comment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeAnnounceEntryComment)));
$this->entityManager->refresh($comment);
self::assertNotNull($comment);
self::assertSame(0, $comment->countDownVotes());
}
public function testDislikeRemotePostInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertSame(0, $post->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeAnnouncePost)));
$this->entityManager->refresh($post);
self::assertNotNull($post);
self::assertSame(1, $post->countDownVotes());
}
#[Depends('testDislikeRemotePostInRemoteMagazine')]
public function testUndoLikeRemotePostInRemoteMagazine(): void
{
$this->testDislikeRemotePostInRemoteMagazine();
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertSame(1, $post->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeAnnouncePost)));
$this->entityManager->refresh($post);
self::assertNotNull($post);
self::assertSame(0, $post->countDownVotes());
}
public function testDislikeRemotePostCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertSame(0, $postComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeAnnouncePostComment)));
$this->entityManager->refresh($postComment);
self::assertNotNull($postComment);
self::assertSame(1, $postComment->countDownVotes());
}
#[Depends('testDislikeRemotePostCommentInRemoteMagazine')]
public function testUndoLikeRemotePostCommentInRemoteMagazine(): void
{
$this->testDislikeRemotePostCommentInRemoteMagazine();
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertSame(1, $postComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeAnnouncePostComment)));
$this->entityManager->refresh($postComment);
self::assertNotNull($postComment);
self::assertSame(0, $postComment->countDownVotes());
}
public function testDislikeEntryInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertSame(0, $entry->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeCreateEntry)));
$this->entityManager->refresh($entry);
self::assertSame(1, $entry->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Dislike' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Dislike' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->dislikeCreateEntry['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->dislikeCreateEntry['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testDislikeEntryInLocalMagazine')]
public function testUndoLikeEntryInLocalMagazine(): void
{
$this->testDislikeEntryInLocalMagazine();
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertSame(1, $entry->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeCreateEntry)));
$this->entityManager->refresh($entry);
self::assertSame(0, $entry->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Dislike' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoDislikeCreateEntry['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Dislike' activity as the object
self::assertEquals($this->undoDislikeCreateEntry['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->undoDislikeCreateEntry['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testDislikeEntryCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertSame(0, $entryComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeCreateEntryComment)));
$this->entityManager->refresh($entryComment);
self::assertSame(1, $entryComment->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Dislike' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Dislike' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->dislikeCreateEntryComment['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->dislikeCreateEntryComment['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testDislikeEntryCommentInLocalMagazine')]
public function testUndoLikeEntryCommentInLocalMagazine(): void
{
$this->testDislikeEntryCommentInLocalMagazine();
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertSame(1, $entryComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeCreateEntryComment)));
$this->entityManager->refresh($entryComment);
self::assertSame(0, $entryComment->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Dislike' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoDislikeCreateEntryComment['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Dislike' activity as the object
self::assertEquals($this->undoDislikeCreateEntryComment['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->undoDislikeCreateEntryComment['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testDislikePostInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
self::assertSame(0, $post->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeCreatePost)));
$this->entityManager->refresh($post);
self::assertSame(1, $post->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Dislike' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Dislike' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->dislikeCreatePost['id'], $postedUpdateAnnounce['payload']['object']['id']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->dislikeCreatePost['object'], $postedUpdateAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
#[Depends('testDislikePostInLocalMagazine')]
public function testUndoLikePostInLocalMagazine(): void
{
$this->testDislikePostInLocalMagazine();
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
self::assertSame(1, $post->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeCreatePost)));
$this->entityManager->refresh($post);
self::assertSame(0, $post->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Dislike' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoDislikeCreatePost['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Dislike' activity as the object
self::assertEquals($this->undoDislikeCreatePost['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->undoDislikeCreatePost['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testDislikePostCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertSame(0, $postComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->dislikeCreatePostComment)));
$this->entityManager->refresh($postComment);
self::assertSame(1, $postComment->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Dislike' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Dislike' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->dislikeCreatePostComment['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->dislikeCreatePostComment['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testDislikePostCommentInLocalMagazine')]
public function testUndoLikePostCommentInLocalMagazine(): void
{
$this->testDislikePostCommentInLocalMagazine();
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertSame(1, $postComment->countDownVotes());
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoDislikeCreatePostComment)));
$this->entityManager->refresh($postComment);
self::assertSame(0, $postComment->countDownVotes());
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Dislike' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoDislikeCreatePostComment['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Dislike' activity as the object
self::assertEquals($this->undoDislikeCreatePostComment['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Dislike' activity has the url as the object
self::assertEquals($this->undoDislikeCreatePostComment['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function setUpRemoteEntities(): void
{
$this->announceEntry = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildDislikeRemoteEntryInRemoteMagazine($entry));
$this->announceEntryComment = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildDislikeRemoteEntryCommentInRemoteMagazine($comment));
$this->announcePost = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Post $post) => $this->buildDislikeRemotePostInRemoteMagazine($post));
$this->announcePostComment = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildDislikeRemotePostCommentInRemoteMagazine($comment));
$this->createEntry = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildDislikeRemoteEntryInLocalMagazine($entry));
$this->createEntryComment = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildDislikeRemoteEntryCommentInLocalMagazine($comment));
$this->createPost = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Post $post) => $this->buildDislikeRemotePostInLocalMagazine($post));
$this->createPostComment = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildDislikeRemotePostCommentInLocalMagazine($comment));
}
public function buildDislikeRemoteEntryInRemoteMagazine(Entry $entry): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $entry);
$this->dislikeAnnounceEntry = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeAnnounceEntry = $this->activityJsonBuilder->buildActivityJson($undoActivity);
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeAnnounceEntry['type'] = 'Dislike';
$this->undoDislikeAnnounceEntry['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeAnnounceEntry['id']] = $this->dislikeAnnounceEntry;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemoteEntryCommentInRemoteMagazine(EntryComment $comment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $comment);
$this->dislikeAnnounceEntryComment = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeAnnounceEntryComment = $this->activityJsonBuilder->buildActivityJson($undoActivity);
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeAnnounceEntryComment['type'] = 'Dislike';
$this->undoDislikeAnnounceEntryComment['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeAnnounceEntryComment['id']] = $this->dislikeAnnounceEntryComment;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemotePostInRemoteMagazine(Post $post): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $post);
$this->dislikeAnnouncePost = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeAnnouncePost = $this->activityJsonBuilder->buildActivityJson($undoActivity);
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeAnnouncePost['type'] = 'Dislike';
$this->undoDislikeAnnouncePost['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeAnnouncePost['id']] = $this->dislikeAnnouncePost;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemotePostCommentInRemoteMagazine(PostComment $postComment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $postComment);
$this->dislikeAnnouncePostComment = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeAnnouncePostComment = $this->activityJsonBuilder->buildActivityJson($undoActivity);
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeAnnouncePostComment['type'] = 'Dislike';
$this->undoDislikeAnnouncePostComment['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeAnnouncePostComment['id']] = $this->dislikeAnnouncePostComment;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemoteEntryInLocalMagazine(Entry $entry): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $entry);
$this->dislikeCreateEntry = $this->RewriteTargetFieldsToLocal($entry->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeCreateEntry = $this->RewriteTargetFieldsToLocal($entry->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeCreateEntry['type'] = 'Dislike';
$this->undoDislikeCreateEntry['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeCreateEntry['id']] = $this->dislikeCreateEntry;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemoteEntryCommentInLocalMagazine(EntryComment $comment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $comment);
$this->dislikeCreateEntryComment = $this->RewriteTargetFieldsToLocal($comment->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeCreateEntryComment = $this->RewriteTargetFieldsToLocal($comment->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeCreateEntryComment['type'] = 'Dislike';
$this->undoDislikeCreateEntryComment['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeCreateEntryComment['id']] = $this->dislikeCreateEntryComment;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemotePostInLocalMagazine(Post $post): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $post);
$this->dislikeCreatePost = $this->RewriteTargetFieldsToLocal($post->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeCreatePost = $this->RewriteTargetFieldsToLocal($post->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeCreatePost['type'] = 'Dislike';
$this->undoDislikeCreatePost['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeCreatePost['id']] = $this->dislikeCreatePost;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
public function buildDislikeRemotePostCommentInLocalMagazine(PostComment $postComment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $postComment);
$this->dislikeCreatePostComment = $this->RewriteTargetFieldsToLocal($postComment->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity);
$this->undoDislikeCreatePostComment = $this->RewriteTargetFieldsToLocal($postComment->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
// since we do not have outgoing federation of dislikes we cheat that here so we can test our inbox federation
$this->dislikeCreatePostComment['type'] = 'Dislike';
$this->undoDislikeCreatePostComment['object']['type'] = 'Dislike';
$this->testingApHttpClient->activityObjects[$this->dislikeCreatePostComment['id']] = $this->dislikeCreatePostComment;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/FlagHandlerTest.php
================================================
bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$subject = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagAnnounceEntry)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemoteEntryCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$subject = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagAnnounceEntryComment)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemotePostInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$subject = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagAnnouncePost)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemotePostCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$subject = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagAnnouncePostComment)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemoteEntryInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$subject = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagCreateEntry)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemoteEntryCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryComment)));
$subject = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagCreateEntryComment)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemotePostInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$subject = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagCreatePost)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function testFlagRemotePostCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPostComment)));
$subject = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($subject);
$this->bus->dispatch(new ActivityMessage(json_encode($this->flagCreatePostComment)));
$report = $this->reportRepository->findBySubject($subject);
self::assertNotNull($report);
self::assertSame($this->remoteSubscriber->username, $report->reporting->username);
self::assertSame(self::REASON, $report->reason);
}
public function setUpRemoteEntities(): void
{
$this->announceEntry = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildFlagRemoteEntryInRemoteMagazine($entry));
$this->announceEntryComment = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildFlagRemoteEntryCommentInRemoteMagazine($comment));
$this->announcePost = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Post $post) => $this->buildFlagRemotePostInRemoteMagazine($post));
$this->announcePostComment = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildFlagRemotePostCommentInRemoteMagazine($comment));
$this->createEntry = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildFlagRemoteEntryInLocalMagazine($entry));
$this->createEntryComment = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildFlagRemoteEntryCommentInLocalMagazine($comment));
$this->createPost = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Post $post) => $this->buildFlagRemotePostInLocalMagazine($post));
$this->createPostComment = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildFlagRemotePostCommentInLocalMagazine($comment));
}
private function buildFlagRemoteEntryInRemoteMagazine(Entry $entry): void
{
$this->flagAnnounceEntry = $this->createFlagActivity($this->remoteSubscriber, $entry);
}
private function buildFlagRemoteEntryCommentInRemoteMagazine(EntryComment $comment): void
{
$this->flagAnnounceEntryComment = $this->createFlagActivity($this->remoteSubscriber, $comment);
}
private function buildFlagRemotePostInRemoteMagazine(Post $post): void
{
$this->flagAnnouncePost = $this->createFlagActivity($this->remoteSubscriber, $post);
}
private function buildFlagRemotePostCommentInRemoteMagazine(PostComment $comment): void
{
$this->flagAnnouncePostComment = $this->createFlagActivity($this->remoteSubscriber, $comment);
}
private function buildFlagRemoteEntryInLocalMagazine(Entry $entry): void
{
$this->flagCreateEntry = $this->createFlagActivity($this->remoteSubscriber, $entry);
}
private function buildFlagRemoteEntryCommentInLocalMagazine(EntryComment $comment): void
{
$this->flagCreateEntryComment = $this->createFlagActivity($this->remoteSubscriber, $comment);
}
private function buildFlagRemotePostInLocalMagazine(Post $post): void
{
$this->flagCreatePost = $this->createFlagActivity($this->remoteSubscriber, $post);
}
private function buildFlagRemotePostCommentInLocalMagazine(PostComment $comment): void
{
$this->flagCreatePostComment = $this->createFlagActivity($this->remoteSubscriber, $comment);
}
private function createFlagActivity(Magazine|User $user, ReportInterface $subject): array
{
$dto = new ReportDto();
$dto->subject = $subject;
$dto->reason = self::REASON;
$report = $this->reportManager->report($dto, $user);
$flagActivity = $this->flagFactory->build($report);
$flagActivityJson = $this->activityJsonBuilder->buildActivityJson($flagActivity);
$flagActivityJson['actor'] = str_replace($this->remoteDomain, $this->remoteSubDomain, $flagActivityJson['actor']);
$this->testingApHttpClient->activityObjects[$flagActivityJson['id']] = $flagActivityJson;
$this->entitiesToRemoveAfterSetup[] = $flagActivity;
return $flagActivityJson;
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/FollowHandlerTest.php
================================================
remoteDomain;
$username = 'followUser';
$followUser = $this->getUserByUsername('followUser');
$json = $this->personFactory->create($followUser);
$this->testingApHttpClient->actorObjects[$json['id']] = $json;
$this->followUserApId = $this->personFactory->getActivityPubId($followUser);
$userEvent = new WebfingerResponseEvent(new JsonRd(), "acct:$username@$domain", ['account' => $username]);
$this->eventDispatcher->dispatch($userEvent);
$realDomain = \sprintf(WebFingerFactory::WEBFINGER_URL, 'https', $domain, '', "$username@$domain");
$this->testingApHttpClient->webfingerObjects[$realDomain] = $userEvent->jsonRd->toArray();
$followActivity = $this->followWrapper->build($followUser, $this->localMagazine);
$this->userFollowMagazine = $this->activityJsonBuilder->buildActivityJson($followActivity);
$apId = "https://$this->prev/m/{$this->localMagazine->name}";
$this->userFollowMagazine['object'] = $apId;
$this->userFollowMagazine['to'] = [$apId];
$this->testingApHttpClient->activityObjects[$this->userFollowMagazine['id']] = $this->userFollowMagazine;
$undoFollowActivity = $this->undoWrapper->build($followActivity);
$this->undoUserFollowMagazine = $this->activityJsonBuilder->buildActivityJson($undoFollowActivity);
$this->undoUserFollowMagazine['to'] = [$apId];
$this->undoUserFollowMagazine['object']['to'] = $apId;
$this->undoUserFollowMagazine['object']['object'] = $apId;
$this->testingApHttpClient->activityObjects[$this->undoUserFollowMagazine['id']] = $this->undoUserFollowMagazine;
$followActivity2 = $this->followWrapper->build($followUser, $this->localUser);
$this->userFollowUser = $this->activityJsonBuilder->buildActivityJson($followActivity2);
$apId = "https://$this->prev/u/{$this->localUser->username}";
$this->userFollowUser['object'] = $apId;
$this->userFollowUser['to'] = [$apId];
$this->testingApHttpClient->activityObjects[$this->userFollowUser['id']] = $this->userFollowUser;
$undoFollowActivity2 = $this->undoWrapper->build($followActivity2);
$this->undoUserFollowUser = $this->activityJsonBuilder->buildActivityJson($undoFollowActivity2);
$this->undoUserFollowUser['to'] = [$apId];
$this->undoUserFollowUser['object']['to'] = $apId;
$this->undoUserFollowUser['object']['object'] = $apId;
$this->testingApHttpClient->activityObjects[$this->undoUserFollowUser['id']] = $this->undoUserFollowUser;
$this->entitiesToRemoveAfterSetup[] = $undoFollowActivity2;
$this->entitiesToRemoveAfterSetup[] = $followActivity2;
$this->entitiesToRemoveAfterSetup[] = $undoFollowActivity;
$this->entitiesToRemoveAfterSetup[] = $followActivity;
$this->entitiesToRemoveAfterSetup[] = $followUser;
}
public function testUserFollowUser(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->userFollowUser)));
$this->entityManager->refresh($this->localUser);
$followUser = $this->userRepository->findOneBy(['apProfileId' => $this->followUserApId]);
$this->entityManager->refresh($followUser);
self::assertNotNull($followUser);
self::assertTrue($followUser->isFollower($this->localUser));
self::assertTrue($followUser->isFollowing($this->localUser));
self::assertNotNull($this->userFollowRepository->findOneBy(['follower' => $followUser, 'following' => $this->localUser]));
self::assertNull($this->userFollowRepository->findOneBy(['follower' => $this->localUser, 'following' => $followUser]));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertCount(1, $postedObjects);
self::assertEquals('Accept', $postedObjects[0]['payload']['type']);
self::assertEquals($followUser->apInboxUrl, $postedObjects[0]['inboxUrl']);
self::assertEquals($this->userFollowUser['id'], $postedObjects[0]['payload']['object']['id']);
}
#[Depends('testUserFollowUser')]
public function testUndoUserFollowUser(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->userFollowUser)));
$followUser = $this->userRepository->findOneBy(['apProfileId' => $this->followUserApId]);
$this->entityManager->refresh($followUser);
$this->entityManager->refresh($this->localUser);
$prevPostedObjects = $this->testingApHttpClient->getPostedObjects();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoUserFollowUser)));
$this->entityManager->refresh($this->localUser);
$this->entityManager->refresh($followUser);
self::assertNotNull($followUser);
self::assertFalse($followUser->isFollower($this->localUser));
self::assertFalse($followUser->isFollowing($this->localUser));
self::assertNull($this->userFollowRepository->findOneBy(['follower' => $followUser, 'following' => $this->localUser]));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertEquals(0, \sizeof($prevPostedObjects) - \sizeof($postedObjects));
}
public function testUserFollowMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->userFollowMagazine)));
$this->entityManager->refresh($this->localUser);
$followUser = $this->userRepository->findOneBy(['apProfileId' => $this->followUserApId]);
$this->entityManager->refresh($followUser);
self::assertNotNull($followUser);
$sub = $this->magazineSubscriptionRepository->findOneBy(['user' => $followUser, 'magazine' => $this->localMagazine]);
self::assertNotNull($sub);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertCount(1, $postedObjects);
self::assertEquals('Accept', $postedObjects[0]['payload']['type']);
self::assertEquals($followUser->apInboxUrl, $postedObjects[0]['inboxUrl']);
self::assertEquals($this->userFollowMagazine['id'], $postedObjects[0]['payload']['object']['id']);
}
#[Depends('testUserFollowMagazine')]
public function testUndoUserFollowMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->userFollowMagazine)));
$followUser = $this->userRepository->findOneBy(['apProfileId' => $this->followUserApId]);
$this->entityManager->refresh($followUser);
$this->entityManager->refresh($this->localUser);
$prevPostedObjects = $this->testingApHttpClient->getPostedObjects();
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoUserFollowMagazine)));
$this->entityManager->refresh($this->localUser);
$this->entityManager->refresh($followUser);
self::assertNotNull($followUser);
$sub = $this->magazineSubscriptionRepository->findOneBy(['magazine' => $this->localMagazine, 'user' => $followUser]);
self::assertNull($sub);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertEquals(0, \sizeof($prevPostedObjects) - \sizeof($postedObjects));
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/LikeHandlerTest.php
================================================
bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertSame(0, $entry->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeAnnounceEntry)));
$this->entityManager->refresh($entry);
self::assertNotNull($entry);
self::assertSame(1, $entry->favouriteCount);
}
#[Depends('testLikeRemoteEntryInRemoteMagazine')]
public function testUndoLikeRemoteEntryInRemoteMagazine(): void
{
$this->testLikeRemoteEntryInRemoteMagazine();
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertSame(1, $entry->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeAnnounceEntry)));
$this->entityManager->refresh($entry);
self::assertNotNull($entry);
self::assertSame(0, $entry->favouriteCount);
}
public function testLikeRemoteEntryCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$comment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertSame(0, $comment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeAnnounceEntryComment)));
$this->entityManager->refresh($comment);
self::assertNotNull($comment);
self::assertSame(1, $comment->favouriteCount);
}
#[Depends('testLikeRemoteEntryCommentInRemoteMagazine')]
public function testUndoLikeRemoteEntryCommentInRemoteMagazine(): void
{
$this->testLikeRemoteEntryCommentInRemoteMagazine();
$comment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertSame(1, $comment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeAnnounceEntryComment)));
$this->entityManager->refresh($comment);
self::assertNotNull($comment);
self::assertSame(0, $comment->favouriteCount);
}
public function testLikeRemotePostInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertSame(0, $post->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeAnnouncePost)));
$this->entityManager->refresh($post);
self::assertNotNull($post);
self::assertSame(1, $post->favouriteCount);
}
#[Depends('testLikeRemotePostInRemoteMagazine')]
public function testUndoLikeRemotePostInRemoteMagazine(): void
{
$this->testLikeRemotePostInRemoteMagazine();
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertSame(1, $post->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeAnnouncePost)));
$this->entityManager->refresh($post);
self::assertNotNull($post);
self::assertSame(0, $post->favouriteCount);
}
public function testLikeRemotePostCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertSame(0, $postComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeAnnouncePostComment)));
$this->entityManager->refresh($postComment);
self::assertNotNull($postComment);
self::assertSame(1, $postComment->favouriteCount);
}
#[Depends('testLikeRemotePostCommentInRemoteMagazine')]
public function testUndoLikeRemotePostCommentInRemoteMagazine(): void
{
$this->testLikeRemotePostCommentInRemoteMagazine();
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertSame(1, $postComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeAnnouncePostComment)));
$this->entityManager->refresh($postComment);
self::assertNotNull($postComment);
self::assertSame(0, $postComment->favouriteCount);
}
public function testLikeEntryInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertSame(0, $entry->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeCreateEntry)));
$this->entityManager->refresh($entry);
self::assertSame(1, $entry->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Like' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Like' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->likeCreateEntry['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Like' activity has the url as the object
self::assertEquals($this->likeCreateEntry['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testLikeEntryInLocalMagazine')]
public function testUndoLikeEntryInLocalMagazine(): void
{
$this->testLikeEntryInLocalMagazine();
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertSame(1, $entry->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeCreateEntry)));
$this->entityManager->refresh($entry);
self::assertSame(0, $entry->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Like' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoLikeCreateEntry['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Like' activity as the object
self::assertEquals($this->undoLikeCreateEntry['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Like' activity has the url as the object
self::assertEquals($this->undoLikeCreateEntry['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testLikeEntryCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertSame(0, $entryComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeCreateEntryComment)));
$this->entityManager->refresh($entryComment);
self::assertSame(1, $entryComment->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Like' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Like' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->likeCreateEntryComment['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Like' activity has the url as the object
self::assertEquals($this->likeCreateEntryComment['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testLikeEntryCommentInLocalMagazine')]
public function testUndoLikeEntryCommentInLocalMagazine(): void
{
$this->testLikeEntryCommentInLocalMagazine();
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertSame(1, $entryComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeCreateEntryComment)));
$this->entityManager->refresh($entryComment);
self::assertSame(0, $entryComment->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Like' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoLikeCreateEntryComment['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Like' activity as the object
self::assertEquals($this->undoLikeCreateEntryComment['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Like' activity has the url as the object
self::assertEquals($this->undoLikeCreateEntryComment['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testLikePostInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
self::assertSame(0, $post->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeCreatePost)));
$this->entityManager->refresh($post);
self::assertSame(1, $post->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Like' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Like' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->likeCreatePost['id'], $postedUpdateAnnounce['payload']['object']['id']);
// the 'Like' activity has the url as the object
self::assertEquals($this->likeCreatePost['object'], $postedUpdateAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
#[Depends('testLikePostInLocalMagazine')]
public function testUndoLikePostInLocalMagazine(): void
{
$this->testLikePostInLocalMagazine();
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertNotNull($post);
self::assertSame(1, $post->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeCreatePost)));
$this->entityManager->refresh($post);
self::assertSame(0, $post->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Like' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoLikeCreatePost['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Like' activity as the object
self::assertEquals($this->undoLikeCreatePost['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Like' activity has the url as the object
self::assertEquals($this->undoLikeCreatePost['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function testLikePostCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertSame(0, $postComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->likeCreatePostComment)));
$this->entityManager->refresh($postComment);
self::assertSame(1, $postComment->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Like' === $arr['payload']['object']['type']);
$postedLikeAnnounce = $postedLikeAnnounces[array_key_first($postedLikeAnnounces)];
// the id of the 'Like' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->likeCreatePostComment['id'], $postedLikeAnnounce['payload']['object']['id']);
// the 'Like' activity has the url as the object
self::assertEquals($this->likeCreatePostComment['object'], $postedLikeAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedLikeAnnounce['inboxUrl']);
}
#[Depends('testLikePostCommentInLocalMagazine')]
public function testUndoLikePostCommentInLocalMagazine(): void
{
$this->testLikePostCommentInLocalMagazine();
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertSame(1, $postComment->favouriteCount);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLikeCreatePostComment)));
$this->entityManager->refresh($postComment);
self::assertSame(0, $postComment->favouriteCount);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedUndoLikeAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Undo' === $arr['payload']['object']['type'] && 'Like' === $arr['payload']['object']['object']['type']);
$postedUndoLikeAnnounce = $postedUndoLikeAnnounces[array_key_first($postedUndoLikeAnnounces)];
// the id of the 'Undo' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->undoLikeCreatePostComment['id'], $postedUndoLikeAnnounce['payload']['object']['id']);
// the 'Undo' activity has the 'Like' activity as the object
self::assertEquals($this->undoLikeCreatePostComment['object'], $postedUndoLikeAnnounce['payload']['object']['object']);
// the 'Like' activity has the url as the object
self::assertEquals($this->undoLikeCreatePostComment['object']['object'], $postedUndoLikeAnnounce['payload']['object']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUndoLikeAnnounce['inboxUrl']);
}
public function setUpRemoteEntities(): void
{
$this->announceEntry = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildLikeRemoteEntryInRemoteMagazine($entry));
$this->announceEntryComment = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildLikeRemoteEntryCommentInRemoteMagazine($comment));
$this->announcePost = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Post $post) => $this->buildLikeRemotePostInRemoteMagazine($post));
$this->announcePostComment = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildLikeRemotePostCommentInRemoteMagazine($comment));
$this->createEntry = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildLikeRemoteEntryInLocalMagazine($entry));
$this->createEntryComment = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildLikeRemoteEntryCommentInLocalMagazine($comment));
$this->createPost = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Post $post) => $this->buildLikeRemotePostInLocalMagazine($post));
$this->createPostComment = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildLikeRemotePostCommentInLocalMagazine($comment));
}
public function buildLikeRemoteEntryInRemoteMagazine(Entry $entry): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $entry);
$this->likeAnnounceEntry = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoLikeActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeAnnounceEntry = $this->activityJsonBuilder->buildActivityJson($undoLikeActivity);
$this->testingApHttpClient->activityObjects[$this->likeAnnounceEntry['id']] = $this->likeAnnounceEntry;
$this->testingApHttpClient->activityObjects[$this->undoLikeAnnounceEntry['id']] = $this->undoLikeAnnounceEntry;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoLikeActivity;
}
public function buildLikeRemoteEntryCommentInRemoteMagazine(EntryComment $comment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $comment);
$this->likeAnnounceEntryComment = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeAnnounceEntryComment = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->testingApHttpClient->activityObjects[$this->likeAnnounceEntryComment['id']] = $this->likeAnnounceEntryComment;
$this->testingApHttpClient->activityObjects[$this->undoLikeAnnounceEntryComment['id']] = $this->undoLikeAnnounceEntryComment;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemotePostInRemoteMagazine(Post $post): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $post);
$this->likeAnnouncePost = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeAnnouncePost = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->testingApHttpClient->activityObjects[$this->likeAnnouncePost['id']] = $this->likeAnnouncePost;
$this->testingApHttpClient->activityObjects[$this->undoLikeAnnouncePost['id']] = $this->undoLikeAnnouncePost;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemotePostCommentInRemoteMagazine(PostComment $postComment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $postComment);
$this->likeAnnouncePostComment = $this->activityJsonBuilder->buildActivityJson($likeActivity);
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeAnnouncePostComment = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->testingApHttpClient->activityObjects[$this->likeAnnouncePostComment['id']] = $this->likeAnnouncePostComment;
$this->testingApHttpClient->activityObjects[$this->undoLikeAnnouncePostComment['id']] = $this->undoLikeAnnouncePostComment;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemoteEntryInLocalMagazine(Entry $entry): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $entry);
$this->likeCreateEntry = $this->RewriteTargetFieldsToLocal($entry->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeCreateEntry = $this->RewriteTargetFieldsToLocal($entry->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
$this->testingApHttpClient->activityObjects[$this->likeCreateEntry['id']] = $this->likeCreateEntry;
$this->testingApHttpClient->activityObjects[$this->undoLikeCreateEntry['id']] = $this->undoLikeCreateEntry;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemoteEntryCommentInLocalMagazine(EntryComment $comment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $comment);
$this->likeCreateEntryComment = $this->RewriteTargetFieldsToLocal($comment->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeCreateEntryComment = $this->RewriteTargetFieldsToLocal($comment->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
$this->testingApHttpClient->activityObjects[$this->likeCreateEntryComment['id']] = $this->likeCreateEntryComment;
$this->testingApHttpClient->activityObjects[$this->undoLikeCreateEntryComment['id']] = $this->undoLikeCreateEntryComment;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemotePostInLocalMagazine(Post $post): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $post);
$this->likeCreatePost = $this->RewriteTargetFieldsToLocal($post->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeCreatePost = $this->RewriteTargetFieldsToLocal($post->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
$this->testingApHttpClient->activityObjects[$this->likeCreatePost['id']] = $this->likeCreatePost;
$this->testingApHttpClient->activityObjects[$this->undoLikeCreatePost['id']] = $this->undoLikeCreatePost;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
public function buildLikeRemotePostCommentInLocalMagazine(PostComment $postComment): void
{
$likeActivity = $this->likeWrapper->build($this->remoteUser, $postComment);
$this->likeCreatePostComment = $this->RewriteTargetFieldsToLocal($postComment->magazine, $this->activityJsonBuilder->buildActivityJson($likeActivity));
$undoActivity = $this->undoWrapper->build($likeActivity, $this->remoteUser);
$this->undoLikeCreatePostComment = $this->RewriteTargetFieldsToLocal($postComment->magazine, $this->activityJsonBuilder->buildActivityJson($undoActivity));
$this->testingApHttpClient->activityObjects[$this->likeCreatePostComment['id']] = $this->likeCreatePostComment;
$this->testingApHttpClient->activityObjects[$this->undoLikeCreatePostComment['id']] = $this->undoLikeCreatePostComment;
$this->entitiesToRemoveAfterSetup[] = $likeActivity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/LockHandlerTest.php
================================================
createLocalEntryAndCreateLockActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
/** @var Entry $entry */
$entry = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Lock', $activity['id']);
$this->bus->dispatch(new ActivityMessage(json_encode($obj['undo'])));
$this->entityManager->refresh($entry);
self::assertFalse($entry->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Undo', $obj['undo']['id']);
}
public function testLockLocalEntryInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalEntryAndCreateLockActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
/** @var Entry $entry */
$entry = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($entry);
self::assertTrue($entry->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Lock');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$this->bus->dispatch(new ActivityMessage(json_encode($obj['undo'])));
$this->entityManager->refresh($entry);
self::assertFalse($entry->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Undo');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
}
public function testLockRemoteEntryInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entryApId = $this->createRemoteEntryInLocalMagazine['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->lockRemoteEntryByRemoteModeratorInLocalMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertTrue($entry->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Lock', $this->lockRemoteEntryByRemoteModeratorInLocalMagazine['id']);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLockRemoteEntryByRemoteModeratorInLocalMagazine)));
$this->entityManager->refresh($entry);
self::assertFalse($entry->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Undo', $this->undoLockRemoteEntryByRemoteModeratorInLocalMagazine['id']);
}
public function testLockRemoteEntryInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entryApId = $this->createRemoteEntryInRemoteMagazine['object']['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->lockRemoteEntryByRemoteModeratorInRemoteMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertTrue($entry->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Lock');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$lockActivities = $this->activityRepository->findBy(['type' => 'Lock']);
self::assertEmpty($lockActivities);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLockRemoteEntryByRemoteModeratorInRemoteMagazine)));
$this->entityManager->refresh($entry);
self::assertFalse($entry->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Undo');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
}
public function testLockLocalPostInLocalMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostAndCreateLockActivity($this->localMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$post = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($post);
self::assertTrue($post->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Lock', $activity['id']);
$this->bus->dispatch(new ActivityMessage(json_encode($obj['undo'])));
$this->entityManager->refresh($post);
self::assertFalse($post->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Undo', $obj['undo']['id']);
}
public function testLockLocalPostInRemoteMagazineByRemoteModerator(): void
{
$obj = $this->createLocalPostAndCreateLockActivity($this->remoteMagazine, $this->localUser, $this->remoteUser);
$activity = $obj['activity'];
$post = $obj['content'];
$this->bus->dispatch(new ActivityMessage(json_encode($activity)));
$this->entityManager->refresh($post);
self::assertTrue($post->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Lock');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$this->bus->dispatch(new ActivityMessage(json_encode($obj['undo'])));
$this->entityManager->refresh($post);
self::assertFalse($post->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Undo');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
}
public function testLockRemotePostInLocalMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$postApId = $this->createRemotePostInLocalMagazine['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->lockRemotePostByRemoteModeratorInLocalMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertTrue($post->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Lock', $this->lockRemotePostByRemoteModeratorInLocalMagazine['id']);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLockRemotePostByRemoteModeratorInLocalMagazine)));
$this->entityManager->refresh($post);
self::assertFalse($post->isLocked);
$this->assertOneSentAnnouncedActivityOfType('Undo', $this->undoLockRemotePostByRemoteModeratorInLocalMagazine['id']);
}
public function testLockRemotePostInRemoteMagazineByRemoteModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$postApId = $this->createRemotePostInRemoteMagazine['object']['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->lockRemotePostByRemoteModeratorInRemoteMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertTrue($post->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Lock');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
$lockActivities = $this->activityRepository->findBy(['type' => 'Lock']);
self::assertEmpty($lockActivities);
$this->bus->dispatch(new ActivityMessage(json_encode($this->undoLockRemotePostByRemoteModeratorInRemoteMagazine)));
$this->entityManager->refresh($post);
self::assertFalse($post->isLocked);
$this->assertCountOfSentActivitiesOfType(0, 'Undo');
$this->assertCountOfSentActivitiesOfType(0, 'Announce');
}
public function setUp(): void
{
parent::setUp();
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->remoteUser));
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->localUser));
$this->magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteUser, $this->localMagazine->getOwner()));
$this->magazineManager->subscribe($this->remoteMagazine, $this->remoteSubscriber);
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$username = 'remotePoster';
$domain = $this->remoteDomain;
$this->remotePoster = $this->getUserByUsername($username, addImage: false);
$this->registerActor($this->remotePoster, $domain, true);
}
public function setUpRemoteEntities(): void
{
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($entry) => $this->createLockFromRemoteEntryInRemoteMagazine($entry));
$this->createRemotePostInRemoteMagazine = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remotePoster, fn ($post) => $this->createLockFromRemotePostInRemoteMagazine($post));
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($entry) => $this->createLockFromRemoteEntryInLocalMagazine($entry));
$this->createRemotePostInLocalMagazine = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remotePoster, fn ($post) => $this->createLockFromRemotePostInLocalMagazine($post));
}
private function createLockFromRemoteEntryInRemoteMagazine(Entry $createdEntry): void
{
$activities = $this->createLockAndUnlockForContent($createdEntry);
$this->lockRemoteEntryByRemoteModeratorInRemoteMagazine = $activities['lock'];
$this->undoLockRemoteEntryByRemoteModeratorInRemoteMagazine = $activities['unlock'];
}
private function createLockFromRemoteEntryInLocalMagazine(Entry $createdEntry): void
{
$activities = $this->createLockAndUnlockForContent($createdEntry);
$this->lockRemoteEntryByRemoteModeratorInLocalMagazine = $activities['lock'];
$this->undoLockRemoteEntryByRemoteModeratorInLocalMagazine = $activities['unlock'];
}
private function createLockFromRemotePostInRemoteMagazine(Post $post): void
{
$activities = $this->createLockAndUnlockForContent($post);
$this->lockRemotePostByRemoteModeratorInRemoteMagazine = $activities['lock'];
$this->undoLockRemotePostByRemoteModeratorInRemoteMagazine = $activities['unlock'];
}
private function createLockFromRemotePostInLocalMagazine(Post $ost): void
{
$activities = $this->createLockAndUnlockForContent($ost);
$this->lockRemotePostByRemoteModeratorInLocalMagazine = $activities['lock'];
$this->undoLockRemotePostByRemoteModeratorInLocalMagazine = $activities['unlock'];
}
/**
* @return array{lock: array, unlock: array}
*/
private function createLockAndUnlockForContent(Entry|Post $content): array
{
$activity = $this->lockFactory->build($this->remoteUser, $content);
$lock = $this->activityJsonBuilder->buildActivityJson($activity);
$undoActivity = $this->undoWrapper->build($activity);
$unlock = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$this->testingApHttpClient->activityObjects[$lock['id']] = $lock;
$this->testingApHttpClient->activityObjects[$unlock['id']] = $unlock;
$this->entitiesToRemoveAfterSetup[] = $activity;
$this->entitiesToRemoveAfterSetup[] = $undoActivity;
return [
'lock' => $lock,
'unlock' => $unlock,
];
}
/**
* @return array{entry: Entry, activity: array, undo: array}
*/
private function createLocalEntryAndCreateLockActivity(Magazine $magazine, User $author, User $lockingUser): array
{
$entry = $this->getEntryByTitle('localEntry', magazine: $magazine, user: $author);
$entryJson = $this->pageFactory->create($entry, [], false);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->lockFactory->build($lockingUser, $entry);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $entryJson['id'];
$undoActivity = $this->undoWrapper->build($activity);
$undoJson = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$undoJson['object']['object'] = $entryJson['id'];
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
$this->entityManager->remove($undoActivity);
return [
'activity' => $activityJson,
'content' => $entry,
'undo' => $undoJson,
];
}
/**
* @return array{content:Post, activity: array, undo: array}
*/
private function createLocalPostAndCreateLockActivity(Magazine $magazine, User $author, User $lockingUser): array
{
$post = $this->createPost('localPost', magazine: $magazine, user: $author);
$postJson = $this->postNoteFactory->create($post, []);
$this->switchToRemoteDomain($this->remoteDomain);
$activity = $this->lockFactory->build($lockingUser, $post);
$activityJson = $this->activityJsonBuilder->buildActivityJson($activity);
$activityJson['object'] = $postJson['id'];
$undoActivity = $this->undoWrapper->build($activity);
$undoJson = $this->activityJsonBuilder->buildActivityJson($undoActivity);
$undoJson['object']['object'] = $postJson['id'];
$this->switchToLocalDomain();
$this->entityManager->remove($activity);
$this->entityManager->remove($undoActivity);
return [
'activity' => $activityJson,
'content' => $post,
'undo' => $undoJson,
];
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/RemoveHandlerTest.php
================================================
remoteMagazine = $this->activityPubManager->findActorOrCreate('!remoteMagazine@remote.mbin');
$this->remoteUser = $this->activityPubManager->findActorOrCreate('@remoteUser@remote.mbin');
// it is important that the moderators are initialized here, as they would be removed from the db if added in `setupRemoteEntries`
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->remoteUser, $this->remoteMagazine->getOwner()));
$this->magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteUser, $this->localUser));
$this->magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->remoteSubscriber, $this->remoteMagazine->getOwner()));
$this->magazineManager->addModerator(new ModeratorDto($this->localMagazine, $this->remoteSubscriber, $this->localUser));
}
public function testRemoveModeratorInRemoteMagazine(): void
{
self::assertTrue($this->remoteMagazine->userIsModerator($this->remoteSubscriber));
$this->bus->dispatch(new ActivityMessage(json_encode($this->removeModeratorRemoteMagazine)));
self::assertFalse($this->remoteMagazine->userIsModerator($this->remoteSubscriber));
}
public function testRemoveModeratorLocalMagazine(): void
{
self::assertTrue($this->localMagazine->userIsModerator($this->remoteSubscriber));
$this->bus->dispatch(new ActivityMessage(json_encode($this->removeModeratorLocalMagazine)));
self::assertFalse($this->localMagazine->userIsModerator($this->remoteSubscriber));
$this->assertRemoveSentToSubscriber($this->removeModeratorLocalMagazine);
}
public function testRemovePinnedEntryInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInRemoteMagazine['object']['object']['id']]);
self::assertNotNull($entry);
$entry->sticky = true;
$this->entityManager->flush();
$this->bus->dispatch(new ActivityMessage(json_encode($this->removePinnedEntryRemoteMagazine)));
$this->entityManager->refresh($entry);
self::assertFalse($entry->sticky);
}
public function testRemovePinnedEntryLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInLocalMagazine['object']['id']]);
self::assertNotNull($entry);
$entry->sticky = true;
$this->entityManager->flush();
$this->bus->dispatch(new ActivityMessage(json_encode($this->removePinnedEntryLocalMagazine)));
$this->entityManager->refresh($entry);
self::assertFalse($entry->sticky);
$this->assertRemoveSentToSubscriber($this->removePinnedEntryLocalMagazine);
}
public function setUpRemoteEntities(): void
{
$this->buildRemoveModeratorInRemoteMagazine();
$this->buildRemoveModeratorInLocalMagazine();
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildRemovePinnedPostInRemoteMagazine($entry));
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildRemovePinnedPostInLocalMagazine($entry));
}
private function buildRemoveModeratorInRemoteMagazine(): void
{
$removeActivity = $this->addRemoveFactory->buildRemoveModerator($this->remoteUser, $this->remoteSubscriber, $this->remoteMagazine);
$this->removeModeratorRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($removeActivity);
$this->removeModeratorRemoteMagazine['object'] = 'https://remote.sub.mbin/u/remoteSubscriber';
$this->testingApHttpClient->activityObjects[$this->removeModeratorRemoteMagazine['id']] = $this->removeModeratorRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $removeActivity;
}
private function buildRemoveModeratorInLocalMagazine(): void
{
$removeActivity = $this->addRemoveFactory->buildRemoveModerator($this->remoteUser, $this->remoteSubscriber, $this->localMagazine);
$this->removeModeratorLocalMagazine = $this->activityJsonBuilder->buildActivityJson($removeActivity);
$this->removeModeratorLocalMagazine['target'] = 'https://kbin.test/m/magazine/moderators';
$this->removeModeratorLocalMagazine['object'] = 'https://remote.sub.mbin/u/remoteSubscriber';
$this->testingApHttpClient->activityObjects[$this->removeModeratorLocalMagazine['id']] = $this->removeModeratorLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $removeActivity;
}
private function buildRemovePinnedPostInRemoteMagazine(Entry $entry): void
{
$removeActivity = $this->addRemoveFactory->buildRemovePinnedPost($this->remoteUser, $entry);
$this->removePinnedEntryRemoteMagazine = $this->activityJsonBuilder->buildActivityJson($removeActivity);
$this->testingApHttpClient->activityObjects[$this->removePinnedEntryRemoteMagazine['id']] = $this->removePinnedEntryRemoteMagazine;
$this->entitiesToRemoveAfterSetup[] = $removeActivity;
}
private function buildRemovePinnedPostInLocalMagazine(Entry $entry): void
{
$removeActivity = $this->addRemoveFactory->buildRemovePinnedPost($this->remoteUser, $entry);
$this->removePinnedEntryLocalMagazine = $this->activityJsonBuilder->buildActivityJson($removeActivity);
$this->removePinnedEntryLocalMagazine['target'] = 'https://kbin.test/m/magazine/pinned';
$this->testingApHttpClient->activityObjects[$this->removePinnedEntryLocalMagazine['id']] = $this->removePinnedEntryLocalMagazine;
$this->entitiesToRemoveAfterSetup[] = $removeActivity;
}
private function assertRemoveSentToSubscriber(array $originalPayload): void
{
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedAddAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Remove' === $arr['payload']['object']['type']);
$postedAddAnnounce = $postedAddAnnounces[array_key_first($postedAddAnnounces)];
// the id of the 'Remove' activity should be wrapped in an 'Announce' activity
self::assertEquals($originalPayload['id'], $postedAddAnnounce['payload']['object']['id']);
self::assertEquals($originalPayload['object'], $postedAddAnnounce['payload']['object']['object']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedAddAnnounce['inboxUrl']);
}
}
================================================
FILE: tests/Functional/ActivityPub/Inbox/UpdateHandlerTest.php
================================================
bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->announceEntry['object']['object']['id']]);
self::assertStringNotContainsString('update', $entry->title);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateAnnounceEntry)));
$this->entityManager->refresh($entry);
self::assertNotNull($entry);
self::assertStringContainsString('update', $entry->title);
self::assertStringContainsString('update', $entry->body);
self::assertFalse($entry->isLocked);
}
public function testUpdateRemoteEntryCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announceEntryComment)));
$comment = $this->entryCommentRepository->findOneBy(['apId' => $this->announceEntryComment['object']['object']['id']]);
self::assertStringNotContainsString('update', $comment->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateAnnounceEntryComment)));
$this->entityManager->refresh($comment);
self::assertNotNull($comment);
self::assertStringContainsString('update', $comment->body);
}
public function testUpdateRemotePostInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$post = $this->postRepository->findOneBy(['apId' => $this->announcePost['object']['object']['id']]);
self::assertStringNotContainsString('update', $post->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateAnnouncePost)));
$this->entityManager->refresh($post);
self::assertNotNull($post);
self::assertStringContainsString('update', $post->body);
self::assertFalse($post->isLocked);
}
public function testUpdateRemotePostCommentInRemoteMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->announcePostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->announcePostComment['object']['object']['id']]);
self::assertStringNotContainsString('update', $postComment->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateAnnouncePostComment)));
$this->entityManager->refresh($postComment);
self::assertNotNull($postComment);
self::assertStringContainsString('update', $postComment->body);
}
public function testUpdateEntryInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createEntry['object']['id']]);
self::assertStringNotContainsString('update', $entry->title);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateCreateEntry)));
self::assertStringContainsString('update', $entry->title);
// explicitly set in the build method
self::assertTrue($entry->isLocked);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Update' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Update' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->updateCreateEntry['id'], $postedUpdateAnnounce['payload']['object']['id']);
self::assertEquals($this->updateCreateEntry['object']['id'], $postedUpdateAnnounce['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
public function testUpdateEntryCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntry)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createEntryComment)));
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $this->createEntryComment['object']['id']]);
self::assertNotNull($entryComment);
self::assertStringNotContainsString('update', $entryComment->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateCreateEntryComment)));
self::assertStringContainsString('update', $entryComment->body);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Update' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Update' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->updateCreateEntryComment['id'], $postedUpdateAnnounce['payload']['object']['id']);
self::assertEquals($this->updateCreateEntryComment['object']['id'], $postedUpdateAnnounce['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
public function testUpdatePostInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$post = $this->postRepository->findOneBy(['apId' => $this->createPost['object']['id']]);
self::assertStringNotContainsString('update', $post->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateCreatePost)));
self::assertStringContainsString('update', $post->body);
// explicitly set in the build method
self::assertTrue($post->isLocked);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Update' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Update' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->updateCreatePost['id'], $postedUpdateAnnounce['payload']['object']['id']);
self::assertEquals($this->updateCreatePost['object']['id'], $postedUpdateAnnounce['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
public function testUpdatePostCommentInLocalMagazine(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPost)));
$this->bus->dispatch(new ActivityMessage(json_encode($this->createPostComment)));
$postComment = $this->postCommentRepository->findOneBy(['apId' => $this->createPostComment['object']['id']]);
self::assertNotNull($postComment);
self::assertStringNotContainsString('update', $postComment->body);
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateCreatePostComment)));
self::assertStringContainsString('update', $postComment->body);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
self::assertNotEmpty($postedObjects);
$postedUpdateAnnounces = array_filter($postedObjects, fn ($arr) => 'Announce' === $arr['payload']['type'] && 'Update' === $arr['payload']['object']['type']);
$postedUpdateAnnounce = $postedUpdateAnnounces[array_key_first($postedUpdateAnnounces)];
// the id of the 'Update' activity should be wrapped in an 'Announce' activity
self::assertEquals($this->updateCreatePostComment['id'], $postedUpdateAnnounce['payload']['object']['id']);
self::assertEquals($this->updateCreatePostComment['object']['id'], $postedUpdateAnnounce['payload']['object']['object']['id']);
self::assertEquals($this->remoteSubscriber->apInboxUrl, $postedUpdateAnnounce['inboxUrl']);
}
public function testUpdateRemoteUser(): void
{
// an update activity forces to fetch the remote object again -> rewrite the actor id to the updated object from the activity
$this->testingApHttpClient->actorObjects[$this->updateUser['object']['id']] = $this->updateUser['object'];
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateUser)));
$user = $this->userRepository->findOneBy(['apPublicUrl' => $this->updateUser['object']['id']]);
self::assertNotNull($user);
self::assertStringContainsString('update', $user->about);
self::assertNotNull($user->publicKey);
self::assertStringContainsString('new public key', $user->publicKey);
self::assertNotNull($user->lastKeyRotationDate);
}
public function testUpdateRemoteUserTitle(): void
{
// an update activity forces to fetch the remote object again -> rewrite the actor id to the updated object from the activity
$object = $this->updateUser['object'];
$object['name'] = 'Test User';
$this->testingApHttpClient->actorObjects[$this->updateUser['object']['id']] = $object;
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateUser)));
$user = $this->userRepository->findOneBy(['apPublicUrl' => $this->updateUser['object']['id']]);
self::assertNotNull($user);
self::assertEquals('Test User', $user->title);
$object = $this->updateUser['object'];
unset($object['name']);
$this->testingApHttpClient->actorObjects[$this->updateUser['object']['id']] = $object;
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateUser)));
$user = $this->userRepository->findOneBy(['apPublicUrl' => $this->updateUser['object']['id']]);
self::assertNotNull($user);
self::assertNull($user->title);
}
public function testUpdateRemoteMagazine(): void
{
// an update activity forces to fetch the remote object again -> rewrite the actor id to the updated object from the activity
$this->testingApHttpClient->actorObjects[$this->updateMagazine['object']['id']] = $this->updateMagazine['object'];
$this->bus->dispatch(new ActivityMessage(json_encode($this->updateMagazine)));
$magazine = $this->magazineRepository->findOneBy(['apPublicUrl' => $this->updateMagazine['object']['id']]);
self::assertNotNull($magazine);
self::assertStringContainsString('update', $magazine->description);
self::assertNotNull($magazine->publicKey);
self::assertStringContainsString('new public key', $magazine->publicKey);
self::assertNotNull($magazine->lastKeyRotationDate);
}
public function setUpRemoteEntities(): void
{
$this->announceEntry = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildUpdateRemoteEntryInRemoteMagazine($entry));
$this->announceEntryComment = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildUpdateRemoteEntryCommentInRemoteMagazine($comment));
$this->announcePost = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (Post $post) => $this->buildUpdateRemotePostInRemoteMagazine($post));
$this->announcePostComment = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildUpdateRemotePostCommentInRemoteMagazine($comment));
$this->createEntry = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Entry $entry) => $this->buildUpdateRemoteEntryInLocalMagazine($entry));
$this->createEntryComment = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (EntryComment $comment) => $this->buildUpdateRemoteEntryCommentInLocalMagazine($comment));
$this->createPost = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remoteUser, fn (Post $post) => $this->buildUpdateRemotePostInLocalMagazine($post));
$this->createPostComment = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remoteUser, fn (PostComment $comment) => $this->buildUpdateRemotePostCommentInLocalMagazine($comment));
$this->buildUpdateUser();
$this->buildUpdateMagazine();
}
public function buildUpdateRemoteEntryInRemoteMagazine(Entry $entry): void
{
$updateActivity = $this->updateWrapper->buildForActivity($entry, $this->remoteUser);
$entry->title = 'Some updated title';
$entry->body = 'Some updated body';
$this->updateAnnounceEntry = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->testingApHttpClient->activityObjects[$this->updateAnnounceEntry['id']] = $this->updateAnnounceEntry;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemoteEntryCommentInRemoteMagazine(EntryComment $comment): void
{
$updateActivity = $this->updateWrapper->buildForActivity($comment, $this->remoteUser);
$comment->body = 'Some updated body';
$this->updateAnnounceEntryComment = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->testingApHttpClient->activityObjects[$this->updateAnnounceEntryComment['id']] = $this->updateAnnounceEntryComment;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemotePostInRemoteMagazine(Post $post): void
{
$updateActivity = $this->updateWrapper->buildForActivity($post, $this->remoteUser);
$post->body = 'Some updated body';
$this->updateAnnouncePost = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->testingApHttpClient->activityObjects[$this->updateAnnouncePost['id']] = $this->updateAnnouncePost;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemotePostCommentInRemoteMagazine(PostComment $postComment): void
{
$updateActivity = $this->updateWrapper->buildForActivity($postComment, $this->remoteUser);
$postComment->body = 'Some updated body';
$this->updateAnnouncePostComment = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->testingApHttpClient->activityObjects[$this->updateAnnouncePostComment['id']] = $this->updateAnnouncePostComment;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemoteEntryInLocalMagazine(Entry $entry): void
{
$updateActivity = $this->updateWrapper->buildForActivity($entry, $this->remoteUser);
$titleBefore = $entry->title;
$entry->title = 'Some updated title';
$entry->body = 'Some updated body';
$entry->isLocked = true;
$this->updateCreateEntry = $this->RewriteTargetFieldsToLocal($entry->magazine, $this->activityJsonBuilder->buildActivityJson($updateActivity));
$entry->title = $titleBefore;
$entry->isLocked = false;
$this->testingApHttpClient->activityObjects[$this->updateCreateEntry['id']] = $this->updateCreateEntry;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemoteEntryCommentInLocalMagazine(EntryComment $comment): void
{
$updateActivity = $this->updateWrapper->buildForActivity($comment, $this->remoteUser);
$comment->body = 'Some updated body';
$this->updateCreateEntryComment = $this->RewriteTargetFieldsToLocal($comment->magazine, $this->activityJsonBuilder->buildActivityJson($updateActivity));
$this->testingApHttpClient->activityObjects[$this->updateCreateEntryComment['id']] = $this->updateCreateEntryComment;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemotePostInLocalMagazine(Post $post): void
{
$updateActivity = $this->updateWrapper->buildForActivity($post, $this->remoteUser);
$bodyBefore = $post->body;
$post->body = 'Some updated body';
$post->isLocked = true;
$this->updateCreatePost = $this->RewriteTargetFieldsToLocal($post->magazine, $this->activityJsonBuilder->buildActivityJson($updateActivity));
$post->body = $bodyBefore;
$post->isLocked = false;
$this->testingApHttpClient->activityObjects[$this->updateCreatePost['id']] = $this->updateCreatePost;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateRemotePostCommentInLocalMagazine(PostComment $postComment): void
{
$updateActivity = $this->updateWrapper->buildForActivity($postComment, $this->remoteUser);
$postComment->body = 'Some updated body';
$this->updateCreatePostComment = $this->RewriteTargetFieldsToLocal($postComment->magazine, $this->activityJsonBuilder->buildActivityJson($updateActivity));
$this->testingApHttpClient->activityObjects[$this->updateCreatePostComment['id']] = $this->updateCreatePostComment;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateUser(): void
{
$aboutBefore = $this->remoteUser->about;
$this->remoteUser->about = 'Some updated user description';
$this->remoteUser->publicKey = 'Some new public key';
$this->remoteUser->privateKey = 'Some new private key';
$updateActivity = $this->updateWrapper->buildForActor($this->remoteUser, $this->remoteUser);
$this->updateUser = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->remoteUser->about = $aboutBefore;
$this->testingApHttpClient->activityObjects[$this->updateUser['id']] = $this->updateUser;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
public function buildUpdateMagazine(): void
{
$descriptionBefore = $this->remoteMagazine->description;
$this->remoteMagazine->description = 'Some updated magazine description';
$this->remoteMagazine->publicKey = 'Some new public key';
$this->remoteMagazine->privateKey = 'Some new private key';
$updateActivity = $this->updateWrapper->buildForActor($this->remoteMagazine, $this->remoteMagazine->getOwner());
$this->updateMagazine = $this->activityJsonBuilder->buildActivityJson($updateActivity);
$this->remoteMagazine->description = $descriptionBefore;
$this->testingApHttpClient->activityObjects[$this->updateMagazine['id']] = $this->updateMagazine;
$this->entitiesToRemoveAfterSetup[] = $updateActivity;
}
}
================================================
FILE: tests/Functional/ActivityPub/MarkdownConverterTest.php
================================================
switchToRemoteDomain($domain);
$this->registerActor($this->getUserByUsername('someUser', email: "someUser@$domain"), $domain, true);
$this->registerActor($this->getMagazineByName('someMagazine'), $domain, true);
$this->switchToLocalDomain();
}
public function setUp(): void
{
parent::setUp();
// generate the local user 'someUser'
$user = $this->getUserByUsername('someUser', email: 'someUser@kbin.test');
$this->getMagazineByName('someMagazine', $user);
$mastodonUser = new User('SomeUser@mastodon.tld', 'SomeUser@mastodon.tld', '', 'Person', 'https://mastodon.tld/users/SomeAccount');
$mastodonUser->apPublicUrl = 'https://mastodon.tld/@SomeAccount';
$this->entityManager->persist($mastodonUser);
}
#[DataProvider('htmlMentionsProvider')]
public function testMentions(string $html, array $apTags, array $expectedMentions, string $name): void
{
$converted = $this->apMarkdownConverter->convert($html, $apTags);
$mentions = $this->mentionManager->extract($converted);
assertEquals($expectedMentions, $mentions, message: "Mention test '$name'");
}
public static function htmlMentionsProvider(): array
{
return [
[
'html' => '
@someUser @someUser@kbin.test
',
'apTags' => [
[
'type' => 'Mention',
'href' => 'https://some.domain.tld/u/someUser',
'name' => '@someUser',
],
[
'type' => 'Mention',
'href' => 'https://kbin.test/u/someUser',
'name' => '@someUser@kbin.test',
],
],
'expectedMentions' => ['@someUser@some.domain.tld', '@someUser@kbin.test'],
'name' => 'Local and remote user',
],
[
'html' => '@someMagazine
',
'apTags' => [
[
'type' => 'Mention',
'href' => 'https://some.domain.tld/m/someMagazine',
'name' => '@someMagazine',
],
],
'expectedMentions' => ['@someMagazine@some.domain.tld'],
'name' => 'Magazine mention',
],
[
'html' => '@someMagazine
',
'apTags' => [
[
'type' => 'Mention',
'href' => 'https://kbin.test/m/someMagazine',
'name' => '@someMagazine',
],
],
'expectedMentions' => ['@someMagazine@kbin.test'],
'name' => 'Local magazine mention',
],
[
'html' => '@SomeAccount ',
'apTags' => [
[
'type' => 'Mention',
'href' => 'https://mastodon.tld/users/SomeAccount',
'name' => '@SomeAccount@mastodon.tld',
],
],
'expectedMentions' => ['@SomeAccount@mastodon.tld'],
'name' => 'Mastodon account mention',
],
];
}
}
================================================
FILE: tests/Functional/ActivityPub/Outbox/BlockHandlerTest.php
================================================
localSubscriber = $this->getUserByUsername('localSubscriber', addImage: false);
// so localSubscriber has one interaction with another instance
$this->magazineManager->subscribe($this->remoteMagazine, $this->localSubscriber);
}
public function setUpRemoteEntities(): void
{
}
public function testBanLocalUserLocalMagazineLocalModerator(): void
{
$this->magazineManager->ban($this->localMagazine, $this->localSubscriber, $this->localUser, MagazineBanDto::create(reason: 'test'));
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteSubscriber->apInboxUrl);
self::assertEquals('test', $blockActivity['summary']);
self::assertEquals($this->personFactory->getActivityPubId($this->localSubscriber), $blockActivity['object']);
self::assertEquals($this->groupFactory->getActivityPubId($this->localMagazine), $blockActivity['target']);
}
public function testUndoBanLocalUserLocalMagazineLocalModerator(): void
{
$this->magazineManager->ban($this->localMagazine, $this->localSubscriber, $this->localUser, MagazineBanDto::create(reason: 'test'));
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteSubscriber->apInboxUrl);
$this->magazineManager->unban($this->localMagazine, $this->localSubscriber);
$undoActivity = $this->assertOneSentActivityOfType('Undo', inboxUrl: $this->remoteSubscriber->apInboxUrl);
self::assertEquals($blockActivity['id'], $undoActivity['object']['id']);
}
public function testBanRemoteUserLocalMagazineLocalModerator(): void
{
$this->magazineManager->ban($this->localMagazine, $this->remoteSubscriber, $this->localUser, MagazineBanDto::create(reason: 'test'));
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteSubscriber->apInboxUrl);
self::assertEquals('test', $blockActivity['summary']);
self::assertEquals($this->remoteSubscriber->apProfileId, $blockActivity['object']);
self::assertEquals($this->groupFactory->getActivityPubId($this->localMagazine), $blockActivity['target']);
}
public function testUndoBanRemoteUserLocalMagazineLocalModerator(): void
{
$this->magazineManager->ban($this->localMagazine, $this->remoteSubscriber, $this->localUser, MagazineBanDto::create(reason: 'test'));
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteSubscriber->apInboxUrl);
$this->magazineManager->unban($this->localMagazine, $this->remoteSubscriber);
$undoActivity = $this->assertOneSentActivityOfType('Undo', inboxUrl: $this->remoteSubscriber->apInboxUrl);
self::assertEquals($blockActivity['id'], $undoActivity['object']['id']);
}
public function testBanLocalUserInstanceLocalModerator(): void
{
$this->userManager->ban($this->localSubscriber, $this->localUser, 'test');
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteMagazine->apInboxUrl);
self::assertEquals('test', $blockActivity['summary']);
self::assertEquals($this->personFactory->getActivityPubId($this->localSubscriber), $blockActivity['object']);
self::assertEquals($this->instanceFactory->getTargetUrl(), $blockActivity['target']);
}
public function testUndoBanLocalUserInstanceLocalModerator(): void
{
$this->userManager->ban($this->localSubscriber, $this->localUser, 'test');
$blockActivity = $this->assertOneSentActivityOfType('Block', inboxUrl: $this->remoteMagazine->apInboxUrl);
$this->userManager->unban($this->localSubscriber, $this->localUser, 'test');
$undoActivity = $this->assertOneSentActivityOfType('Undo', inboxUrl: $this->remoteMagazine->apInboxUrl);
self::assertEquals($blockActivity['id'], $undoActivity['object']['id']);
}
}
================================================
FILE: tests/Functional/ActivityPub/Outbox/DeleteHandlerTest.php
================================================
magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->localUser));
$this->localPoster = $this->getUserByUsername('localPoster', addImage: false);
}
public function setUpRemoteEntities(): void
{
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemoteEntryCommentInRemoteMagazine = $this->createRemoteEntryCommentInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemotePostInRemoteMagazine = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemotePostCommentInRemoteMagazine = $this->createRemotePostCommentInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remotePoster);
$this->createRemoteEntryCommentInLocalMagazine = $this->createRemoteEntryCommentInLocalMagazine($this->localMagazine, $this->remotePoster);
$this->createRemotePostInLocalMagazine = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remotePoster);
$this->createRemotePostCommentInLocalMagazine = $this->createRemotePostCommentInLocalMagazine($this->localMagazine, $this->remotePoster);
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$username = 'remotePoster';
$domain = $this->remoteDomain;
$this->remotePoster = $this->getUserByUsername($username, addImage: false);
$this->registerActor($this->remotePoster, $domain, true);
}
public function testDeleteLocalEntryInLocalMagazineByLocalModerator(): void
{
$entry = $this->getEntryByTitle(title: 'test entry', magazine: $this->localMagazine, user: $this->localUser);
$this->entryManager->delete($this->localUser, $entry);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalEntryInRemoteMagazineByLocalModerator(): void
{
$entry = $this->getEntryByTitle(title: 'test entry', magazine: $this->remoteMagazine, user: $this->localUser);
$this->entryManager->delete($this->localUser, $entry);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemoteEntryInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entryApId = $this->createRemoteEntryInLocalMagazine['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->entryManager->delete($this->localUser, $entry);
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertTrue($entry->isTrashed());
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemoteEntryInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entryApId = $this->createRemoteEntryInRemoteMagazine['object']['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->entryManager->purge($this->localUser, $entry);
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNull($entry);
self::assertNotEmpty($this->testingApHttpClient->getPostedObjects());
$deleteActivity = $this->activityRepository->findOneBy(['type' => 'Delete']);
self::assertNotNull($deleteActivity);
$activityId = $this->urlGenerator->generate('ap_object', ['id' => $deleteActivity->uuid], UrlGeneratorInterface::ABSOLUTE_URL);
$this->assertOneSentActivityOfType('Delete', $activityId);
}
public function testDeleteLocalEntryCommentInLocalMagazineByLocalModerator(): void
{
$entry = $this->getEntryByTitle(title: 'test entry', magazine: $this->localMagazine, user: $this->localUser);
$comment = $this->createEntryComment('test entry comment', entry: $entry, user: $this->localUser);
$this->removeActivitiesWithObject($comment);
$this->entryCommentManager->delete($this->localUser, $comment);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalEntryCommentInRemoteMagazineByLocalModerator(): void
{
$entry = $this->getEntryByTitle(title: 'test entry', magazine: $this->remoteMagazine, user: $this->localUser);
$comment = $this->createEntryComment('test entry comment', entry: $entry, user: $this->localUser);
$this->removeActivitiesWithObject($comment);
$this->entryCommentManager->delete($this->localUser, $comment);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemoteEntryCommentInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entryApId = $this->createRemoteEntryInLocalMagazine['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryCommentInLocalMagazine)));
$entryCommentApId = $this->createRemoteEntryCommentInLocalMagazine['object']['id'];
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertNotNull($entryComment);
$this->entryCommentManager->delete($this->localUser, $entryComment);
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertTrue($entryComment->isTrashed());
// 2 subs -> 2 delete activities
$this->assertCountOfSentActivitiesOfType(2, 'Delete');
}
public function testDeleteRemoteEntryCommentInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entryApId = $this->createRemoteEntryInRemoteMagazine['object']['object']['id'];
$entry = $this->entryRepository->findOneBy(['apId' => $entryApId]);
self::assertNotNull($entry);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryCommentInRemoteMagazine)));
$entryCommentApId = $this->createRemoteEntryCommentInRemoteMagazine['object']['object']['id'];
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertNotNull($entryComment);
$this->entryCommentManager->purge($this->localUser, $entryComment);
$entryComment = $this->entryCommentRepository->findOneBy(['apId' => $entryCommentApId]);
self::assertNull($entryComment);
self::assertNotEmpty($this->testingApHttpClient->getPostedObjects());
$deleteActivity = $this->activityRepository->findOneBy(['type' => 'Delete']);
self::assertNotNull($deleteActivity);
$activityId = $this->urlGenerator->generate('ap_object', ['id' => $deleteActivity->uuid], UrlGeneratorInterface::ABSOLUTE_URL);
$this->assertOneSentActivityOfType('Delete', $activityId);
}
public function testDeleteLocalPostInLocalMagazineByLocalModerator(): void
{
$post = $this->createPost(body: 'test post', magazine: $this->localMagazine, user: $this->localUser);
$this->postManager->delete($this->localUser, $post);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalPostInRemoteMagazineByLocalModerator(): void
{
$post = $this->createPost(body: 'test post', magazine: $this->remoteMagazine, user: $this->localUser);
$this->postManager->delete($this->localUser, $post);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemotePostInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$postApId = $this->createRemotePostInLocalMagazine['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->postManager->delete($this->localUser, $post);
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertTrue($post->isTrashed());
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemotePostInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$postApId = $this->createRemotePostInRemoteMagazine['object']['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->postManager->purge($this->localUser, $post);
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNull($post);
self::assertNotEmpty($this->testingApHttpClient->getPostedObjects());
$deleteActivity = $this->activityRepository->findOneBy(['type' => 'Delete']);
self::assertNotNull($deleteActivity);
$activityId = $this->urlGenerator->generate('ap_object', ['id' => $deleteActivity->uuid], UrlGeneratorInterface::ABSOLUTE_URL);
$this->assertOneSentActivityOfType('Delete', $activityId);
}
public function testDeleteLocalPostCommentInLocalMagazineByLocalModerator(): void
{
$post = $this->createPost(body: 'test post', magazine: $this->localMagazine, user: $this->localUser);
$comment = $this->createPostComment('test post comment', post: $post, user: $this->localUser);
$this->removeActivitiesWithObject($comment);
$this->postCommentManager->delete($this->localUser, $comment);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalPostCommentInRemoteMagazineByLocalModerator(): void
{
$post = $this->createPost(body: 'test post', magazine: $this->remoteMagazine, user: $this->localUser);
$comment = $this->createPostComment('test post comment', post: $post, user: $this->localUser);
$this->removeActivitiesWithObject($comment);
$this->postCommentManager->delete($this->localUser, $comment);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteRemotePostCommentInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$postApId = $this->createRemotePostInLocalMagazine['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostCommentInLocalMagazine)));
$postCommentApId = $this->createRemotePostCommentInLocalMagazine['object']['id'];
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertNotNull($postComment);
$this->postCommentManager->delete($this->localUser, $postComment);
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertTrue($postComment->isTrashed());
// 2 subs -> 2 delete activities
$this->assertCountOfSentActivitiesOfType(2, 'Delete');
}
public function testDeleteRemotePostCommentInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$postApId = $this->createRemotePostInRemoteMagazine['object']['object']['id'];
$post = $this->postRepository->findOneBy(['apId' => $postApId]);
self::assertNotNull($post);
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostCommentInRemoteMagazine)));
$postCommentApId = $this->createRemotePostCommentInRemoteMagazine['object']['object']['id'];
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertNotNull($postComment);
$this->postCommentManager->purge($this->localUser, $postComment);
$postComment = $this->postCommentRepository->findOneBy(['apId' => $postCommentApId]);
self::assertNull($postComment);
self::assertNotEmpty($this->testingApHttpClient->getPostedObjects());
$deleteActivity = $this->activityRepository->findOneBy(['type' => 'Delete']);
self::assertNotNull($deleteActivity);
$activityId = $this->urlGenerator->generate('ap_object', ['id' => $deleteActivity->uuid], UrlGeneratorInterface::ABSOLUTE_URL);
$this->assertOneSentActivityOfType('Delete', $activityId);
}
public function testDeleteLocalEntryInRemoteMagazineByAuthor(): void
{
$entry = $this->createEntry('test local entry', $this->remoteMagazine, $this->localPoster);
$createEntryActivity = $this->activityRepository->findOneBy(['objectEntry' => $entry]);
$this->entityManager->remove($createEntryActivity);
$this->entryManager->delete($this->localPoster, $entry);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalEntryCommentInRemoteMagazineByAuthor(): void
{
$entry = $this->createEntry('test local entry', $this->remoteMagazine, $this->localPoster);
$entryComment = $this->createEntryComment('test local entryComment', $entry, $this->localPoster);
$createEntryCommentActivity = $this->activityRepository->findOneBy(['objectEntryComment' => $entryComment]);
$this->entityManager->remove($createEntryCommentActivity);
$this->entryCommentManager->delete($this->localPoster, $entryComment);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalPostInRemoteMagazineByAuthor(): void
{
$post = $this->createPost('test local post', $this->remoteMagazine, $this->localPoster);
$createPostActivity = $this->activityRepository->findOneBy(['objectPost' => $post]);
$this->entityManager->remove($createPostActivity);
$this->postManager->delete($this->localPoster, $post);
$this->assertOneSentActivityOfType('Delete');
}
public function testDeleteLocalPostCommentInRemoteMagazineByAuthor(): void
{
$post = $this->createPost('test local post', $this->remoteMagazine, $this->localPoster);
$postComment = $this->createPostComment('test local post comment', $post, $this->localPoster);
$createPostCommentActivity = $this->activityRepository->findOneBy(['objectPostComment' => $postComment]);
$this->entityManager->remove($createPostCommentActivity);
$this->postCommentManager->delete($this->localPoster, $postComment);
$this->assertOneSentActivityOfType('Delete');
}
public function removeActivitiesWithObject(ActivityPubActivityInterface|ActivityPubActorInterface $object): void
{
$activities = $this->activityRepository->findAllActivitiesByObject($object);
foreach ($activities as $activity) {
$this->entityManager->remove($activity);
}
}
}
================================================
FILE: tests/Functional/ActivityPub/Outbox/LockHandlerTest.php
================================================
magazineManager->addModerator(new ModeratorDto($this->remoteMagazine, $this->localUser));
$this->localPoster = $this->getUserByUsername('localPoster', addImage: false);
}
public function setUpRemoteEntities(): void
{
$this->createRemoteEntryInRemoteMagazine = $this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemotePostInRemoteMagazine = $this->createRemotePostInRemoteMagazine($this->remoteMagazine, $this->remotePoster);
$this->createRemoteEntryInLocalMagazine = $this->createRemoteEntryInLocalMagazine($this->localMagazine, $this->remotePoster);
$this->createRemotePostInLocalMagazine = $this->createRemotePostInLocalMagazine($this->localMagazine, $this->remotePoster);
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$username = 'remotePoster';
$domain = $this->remoteDomain;
$this->remotePoster = $this->getUserByUsername($username, addImage: false);
$this->registerActor($this->remotePoster, $domain, true);
}
public function testLockLocalEntryInLocalMagazineByLocalModerator(): void
{
$entry = $this->createEntry('Some local entry', $this->localMagazine, $this->localPoster);
$this->entryManager->toggleLock($entry, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockLocalEntryInRemoteMagazineByLocalModerator(): void
{
$entry = $this->createEntry('Some local entry', $this->remoteMagazine, $this->localPoster);
$this->entryManager->toggleLock($entry, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockRemoteEntryInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInLocalMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInLocalMagazine['object']['id']]);
self::assertNotNull($entry);
$this->entryManager->toggleLock($entry, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockRemoteEntryInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemoteEntryInRemoteMagazine)));
$entry = $this->entryRepository->findOneBy(['apId' => $this->createRemoteEntryInRemoteMagazine['object']['object']['id']]);
self::assertNotNull($entry);
$this->entryManager->toggleLock($entry, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockLocalPostInLocalMagazineByLocalModerator(): void
{
$post = $this->createPost('Some post', $this->localMagazine, $this->localPoster);
$this->postManager->toggleLock($post, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockLocalPostInRemoteMagazineByLocalModerator(): void
{
$post = $this->createPost('Some post', $this->remoteMagazine, $this->localPoster);
$this->postManager->toggleLock($post, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockRemotePostInLocalMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInLocalMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $this->createRemotePostInLocalMagazine['object']['id']]);
self::assertNotNull($post);
$this->postManager->toggleLock($post, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockRemotePostInRemoteMagazineByLocalModerator(): void
{
$this->bus->dispatch(new ActivityMessage(json_encode($this->createRemotePostInRemoteMagazine)));
$post = $this->postRepository->findOneBy(['apId' => $this->createRemotePostInRemoteMagazine['object']['object']['id']]);
self::assertNotNull($post);
$this->postManager->toggleLock($post, $this->localUser);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockLocalEntryInRemoteMagazineByAuthor(): void
{
$entry = $this->createEntry('Some local entry', $this->remoteMagazine, $this->localPoster);
$this->entryManager->toggleLock($entry, $this->localPoster);
$this->assertOneSentActivityOfType('Lock');
}
public function testLockLocalPostInRemoteMagazineByAuthor(): void
{
$post = $this->createPost('Some local post', $this->remoteMagazine, $this->localPoster);
$this->postManager->toggleLock($post, $this->localPoster);
$this->assertOneSentActivityOfType('Lock');
}
}
================================================
FILE: tests/Functional/Command/AdminCommandTest.php
================================================
create('actor', 'contact@example.com');
$dto->plainPassword = 'secret';
$this->getContainer()->get(UserManager::class)
->create($dto, false);
$this->assertFalse($this->repository->findOneByUsername('actor')->isAdmin());
$tester = new CommandTester($this->command);
$tester->execute(['username' => 'actor']);
$this->assertStringContainsString('Administrator privileges have been granted.', $tester->getDisplay());
$this->assertTrue($this->repository->findOneByUsername('actor')->isAdmin());
}
protected function setUp(): void
{
$application = new Application(self::bootKernel());
$this->command = $application->find('mbin:user:admin');
$this->repository = $this->getContainer()->get(UserRepository::class);
}
}
================================================
FILE: tests/Functional/Command/ModeratorCommandTest.php
================================================
create('actor', 'contact@example.com');
$dto->plainPassword = 'secret';
$this->getContainer()->get(UserManager::class)
->create($dto, false);
$this->assertFalse($this->repository->findOneByUsername('actor')->isModerator());
$tester = new CommandTester($this->command);
$tester->execute(['username' => 'actor']);
$this->assertStringContainsString('Global moderator privileges have been granted.', $tester->getDisplay());
$this->assertTrue($this->repository->findOneByUsername('actor')->isModerator());
}
protected function setUp(): void
{
$application = new Application(self::bootKernel());
$this->command = $application->find('mbin:user:moderator');
$this->repository = $this->getContainer()->get(UserRepository::class);
}
}
================================================
FILE: tests/Functional/Command/UserCommandTest.php
================================================
command);
$tester->execute(
[
'username' => 'actor',
'email' => 'contact@example.com',
'password' => 'secret',
]
);
$this->assertStringContainsString('A user has been created.', $tester->getDisplay());
$this->assertInstanceOf(User::class, $this->repository->findOneByUsername('actor'));
}
public function testCreateAdminUser(): void
{
$tester = new CommandTester($this->command);
$tester->execute(
[
'username' => 'actor',
'email' => 'contact@example.com',
'password' => 'secret',
'--admin' => true,
],
);
$this->assertStringContainsString('A user has been created.', $tester->getDisplay());
$actor = $this->repository->findOneByUsername('actor');
$this->assertInstanceOf(User::class, $actor);
$this->assertTrue($actor->isAdmin());
}
protected function setUp(): void
{
$application = new Application(self::bootKernel());
$this->command = $application->find('mbin:user:create');
$this->repository = $this->getContainer()->get(UserRepository::class);
}
}
================================================
FILE: tests/Functional/Controller/ActivityPub/GeneralAPTest.php
================================================
getUserByUsername('user');
$this->client->request('GET', '/u/user', [], [], [
'HTTP_ACCEPT' => $acceptHeader,
]);
self::assertResponseHeaderSame('Content-Type', 'application/activity+json');
}
public static function provideAcceptHeaders(): array
{
return [
['application/ld+json;profile=https://www.w3.org/ns/activitystreams'],
['application/ld+json;profile="https://www.w3.org/ns/activitystreams"'],
['application/ld+json ; profile="https://www.w3.org/ns/activitystreams"'],
['application/ld+json'],
['application/activity+json'],
['application/json'],
];
}
}
================================================
FILE: tests/Functional/Controller/ActivityPub/UserOutboxControllerTest.php
================================================
getUserByUsername('apUser', addImage: false);
$user2 = $this->getUserByUsername('apUser2', addImage: false);
$magazine = $this->getMagazineByName('test-magazine');
// create a message to test that it is not part of the outbox
$dto = new MessageDto();
$dto->body = 'this is a message';
$thread = $this->messageManager->toThread($dto, $user, $user2);
$entry = $this->createEntry('entry', $magazine, user: $user);
$entryComment = $this->createEntryComment('comment', $entry, user: $user);
$post = $this->createPost('post', $magazine, user: $user);
$postComment = $this->createPostComment('comment', $post, user: $user);
// upvote an entry to check that it is not part of the outbox
$entryToLike = $this->getEntryByTitle('test entry 2');
$this->favouriteManager->toggle($user, $entryToLike);
// downvote an entry to check that it is not part of the outbox
$entryToDislike = $this->getEntryByTitle('test entry 3');
$this->voteManager->vote(-1, $entryToDislike, $user);
// boost an entry to check that it is part of the outbox
$entryToDislike = $this->getEntryByTitle('test entry 4');
$this->voteManager->vote(1, $entryToDislike, $user);
}
public function testUserOutbox(): void
{
$this->client->request('GET', '/u/apUser/outbox', server: ['HTTP_ACCEPT' => 'application/activity+json']);
self::assertResponseIsSuccessful();
$json = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::COLLECTION_KEYS, $json);
self::assertEquals('OrderedCollection', $json['type']);
self::assertEquals(5, $json['totalItems']);
$firstPage = $json['first'];
$this->client->request('GET', $firstPage, server: ['HTTP_ACCEPT' => 'application/activity+json']);
self::assertResponseIsSuccessful();
}
public function testUserOutboxPage1(): void
{
$this->client->request('GET', '/u/apUser/outbox?page=1', server: ['HTTP_ACCEPT' => 'application/activity+json']);
self::assertResponseIsSuccessful();
$json = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::COLLECTION_ITEMS_KEYS, $json);
self::assertEquals(5, $json['totalItems']);
self::assertCount(5, $json['orderedItems']);
$entries = array_filter($json['orderedItems'], fn (array $createActivity) => 'Create' === $createActivity['type'] && 'Page' === $createActivity['object']['type']);
self::assertCount(1, $entries);
$entryComments = array_filter($json['orderedItems'], fn (array $createActivity) => 'Create' === $createActivity['type'] && 'Note' === $createActivity['object']['type'] && str_contains($createActivity['object']['inReplyTo'] ?? '', '/t/'));
self::assertCount(1, $entryComments);
$posts = array_filter($json['orderedItems'], fn (array $createActivity) => 'Create' === $createActivity['type'] && 'Note' === $createActivity['object']['type'] && null === $createActivity['object']['inReplyTo']);
self::assertCount(1, $posts);
$postComments = array_filter($json['orderedItems'], fn (array $createActivity) => 'Create' === $createActivity['type'] && 'Note' === $createActivity['object']['type'] && str_contains($createActivity['object']['inReplyTo'] ?? '', '/p/'));
self::assertCount(1, $postComments);
$boosts = array_filter($json['orderedItems'], fn (array $createActivity) => 'Announce' === $createActivity['type']);
self::assertCount(1, $boosts);
// the outbox should not contain ChatMessages, likes or dislikes
$likes = array_filter($json['orderedItems'], fn (array $createActivity) => 'Like' === $createActivity['type']);
self::assertCount(0, $likes);
$dislikes = array_filter($json['orderedItems'], fn (array $createActivity) => 'Dislike' === $createActivity['type']);
self::assertCount(0, $dislikes);
$chatMessages = array_filter($json['orderedItems'], fn (array $createActivity) => 'Create' === $createActivity['type'] && 'ChatMessage' === $createActivity['object']['type']);
self::assertCount(0, $chatMessages);
$ids = array_map(fn (array $createActivity) => $createActivity['id'], $json['orderedItems']);
$this->client->request('GET', '/u/apUser/outbox?page=1', server: ['HTTP_ACCEPT' => 'application/activity+json']);
self::assertResponseIsSuccessful();
$json = self::getJsonResponse($this->client);
$ids2 = array_map(fn (array $createActivity) => $createActivity['id'], $json['orderedItems']);
// check that the ids of the 'Create' activities are stable
self::assertEquals($ids, $ids2);
}
}
================================================
FILE: tests/Functional/Controller/Admin/AdminFederationControllerTest.php
================================================
instanceRepository->getOrCreateInstance('www.example.com');
$this->instanceManager->banInstance($instance);
$this->client->loginUser($this->getUserByUsername('admin', isAdmin: true));
$crawler = $this->client->request('GET', '/admin/federation');
$this->client->submit($crawler->filter('#content tr td button[type=submit]')->form());
$this->assertSame(
[],
$this->settingsManager->getBannedInstances(),
);
}
}
================================================
FILE: tests/Functional/Controller/Admin/AdminUserControllerTest.php
================================================
getUserByUsername('inactiveUser', active: false);
$admin = $this->getUserByUsername('admin', isAdmin: true);
$this->client->loginUser($admin);
$this->client->request('GET', '/admin/users/inactive');
self::assertResponseIsSuccessful();
self::assertAnySelectorTextContains('a.user-inline', 'inactiveUser');
}
}
================================================
FILE: tests/Functional/Controller/Api/Bookmark/BookmarkApiTest.php
================================================
user = $this->getUserByUsername('user');
$this->client->loginUser($this->user);
self::createOAuth2PublicAuthCodeClient();
$codes = self::getPublicAuthorizationCodeTokenResponse($this->client, scopes: 'read bookmark bookmark_list');
$this->token = $codes['token_type'].' '.$codes['access_token'];
// it seems that the oauth flow detaches the user object from the entity manager, so fetch it again
$this->user = $this->userRepository->findOneByUsername('user');
}
public function testBookmarkEntryToDefault(): void
{
$entry = $this->getEntryByTitle('entry');
$this->client->request('PUT', "/api/bos/{$entry->getId()}/entry", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $this->bookmarkListRepository->findOneByUserDefault($this->user));
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkEntryCommentToDefault(): void
{
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$this->client->request('PUT', "/api/bos/{$comment->getId()}/entry_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $this->bookmarkListRepository->findOneByUserDefault($this->user));
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkPostToDefault(): void
{
$post = $this->createPost('post');
$this->client->request('PUT', "/api/bos/{$post->getId()}/post", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $this->bookmarkListRepository->findOneByUserDefault($this->user));
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkPostCommentToDefault(): void
{
$post = $this->createPost('entry');
$comment = $this->createPostComment('comment', $post);
$this->client->request('PUT', "/api/bos/{$comment->getId()}/post_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $this->bookmarkListRepository->findOneByUserDefault($this->user));
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testRemoveBookmarkEntryFromDefault(): void
{
$entry = $this->getEntryByTitle('entry');
$this->client->request('PUT', "/api/bos/{$entry->getId()}/entry", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$list = $this->bookmarkListRepository->findOneByUserDefault($this->user);
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbo/{$entry->getId()}/entry", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkEntryCommentFromDefault(): void
{
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$this->client->request('PUT', "/api/bos/{$comment->getId()}/entry_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$list = $this->bookmarkListRepository->findOneByUserDefault($this->user);
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbo/{$comment->getId()}/entry_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkPostFromDefault(): void
{
$post = $this->createPost('post');
$this->client->request('PUT', "/api/bos/{$post->getId()}/post", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$list = $this->bookmarkListRepository->findOneByUserDefault($this->user);
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbo/{$post->getId()}/post", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkPostCommentFromDefault(): void
{
$post = $this->createPost('entry');
$comment = $this->createPostComment('comment', $post);
$this->client->request('PUT', "/api/bos/{$comment->getId()}/post_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$list = $this->bookmarkListRepository->findOneByUserDefault($this->user);
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbo/{$comment->getId()}/post_comment", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testBookmarkEntryToList(): void
{
$this->entityManager->refresh($this->user);
$list = $this->bookmarkManager->createList($this->user, 'list');
$entry = $this->getEntryByTitle('entry');
$this->client->request('PUT', "/api/bol/{$entry->getId()}/entry/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkEntryCommentToList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$this->client->request('PUT', "/api/bol/{$comment->getId()}/entry_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkPostToList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$post = $this->createPost('post');
$this->client->request('PUT', "/api/bol/{$post->getId()}/post/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testBookmarkPostCommentToList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$post = $this->createPost('entry');
$comment = $this->createPostComment('comment', $post);
$this->client->request('PUT', "/api/bol/{$comment->getId()}/post_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
}
public function testRemoveBookmarkEntryFromList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$entry = $this->getEntryByTitle('entry');
$this->client->request('PUT', "/api/bol/{$entry->getId()}/entry/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbol/{$entry->getId()}/entry/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkEntryCommentFromList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$this->client->request('PUT', "/api/bol/{$comment->getId()}/entry_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbol/{$comment->getId()}/entry_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkPostFromList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$post = $this->createPost('post');
$this->client->request('PUT', "/api/bol/{$post->getId()}/post/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbol/{$post->getId()}/post/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testRemoveBookmarkPostCommentFromList(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$post = $this->createPost('entry');
$comment = $this->createPostComment('comment', $post);
$this->client->request('PUT', "/api/bol/{$comment->getId()}/post_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(1, $bookmarks);
$this->client->request('DELETE', "/api/rbol/{$comment->getId()}/post_comment/$list->name", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$bookmarks = $this->bookmarkRepository->findByList($this->user, $list);
self::assertIsArray($bookmarks);
self::assertCount(0, $bookmarks);
}
public function testBookmarkedEntryJson(): void
{
$entry = $this->getEntryByTitle('entry');
$list = $this->bookmarkManager->createList($this->user, 'list');
$this->bookmarkManager->addBookmarkToDefaultList($this->user, $entry);
$this->bookmarkManager->addBookmark($this->user, $list, $entry);
$this->client->request('GET', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
assertIsArray($jsonData['bookmarks']);
assertCount(2, $jsonData['bookmarks']);
self::assertContains('list', $jsonData['bookmarks']);
}
public function testBookmarkedEntryCommentJson(): void
{
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$list = $this->bookmarkManager->createList($this->user, 'list');
$this->bookmarkManager->addBookmarkToDefaultList($this->user, $comment);
$this->bookmarkManager->addBookmark($this->user, $list, $comment);
$this->client->request('GET', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
assertIsArray($jsonData['bookmarks']);
assertCount(2, $jsonData['bookmarks']);
self::assertContains('list', $jsonData['bookmarks']);
}
public function testBookmarkedPostJson(): void
{
$post = $this->createPost('post');
$list = $this->bookmarkManager->createList($this->user, 'list');
$this->bookmarkManager->addBookmarkToDefaultList($this->user, $post);
$this->bookmarkManager->addBookmark($this->user, $list, $post);
$this->client->request('GET', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
assertIsArray($jsonData['bookmarks']);
assertCount(2, $jsonData['bookmarks']);
self::assertContains('list', $jsonData['bookmarks']);
}
public function testBookmarkedPostCommentJson(): void
{
$post = $this->createPost('post');
$comment = $this->createPostComment('comment', $post);
$list = $this->bookmarkManager->createList($this->user, 'list');
$this->bookmarkManager->addBookmarkToDefaultList($this->user, $comment);
$this->bookmarkManager->addBookmark($this->user, $list, $comment);
$this->client->request('GET', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
assertIsArray($jsonData['bookmarks']);
assertCount(2, $jsonData['bookmarks']);
self::assertContains('list', $jsonData['bookmarks']);
}
public function testBookmarkListFront(): void
{
$list = $this->bookmarkManager->createList($this->user, 'list');
$entry = $this->getEntryByTitle('entry');
$comment = $this->createEntryComment('comment', $entry);
$comment2 = $this->createEntryComment('coment2', $entry, parent: $comment);
$post = $this->createPost('post');
$postComment = $this->createPostComment('comment', $post);
$postComment2 = $this->createPostComment('comment2', $post, parent: $postComment);
$this->bookmarkManager->addBookmark($this->user, $list, $entry);
$this->bookmarkManager->addBookmark($this->user, $list, $comment);
$this->bookmarkManager->addBookmark($this->user, $list, $comment2);
$this->bookmarkManager->addBookmark($this->user, $list, $post);
$this->bookmarkManager->addBookmark($this->user, $list, $postComment);
$this->bookmarkManager->addBookmark($this->user, $list, $postComment2);
$this->client->request('GET', "/api/bookmark-lists/show?list={$list->name}", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
assertIsArray($jsonData['items']);
assertCount(6, $jsonData['items']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Bookmark/BookmarkListApiTest.php
================================================
user = $this->getUserByUsername('user');
$this->client->loginUser($this->user);
self::createOAuth2PublicAuthCodeClient();
$codes = self::getPublicAuthorizationCodeTokenResponse($this->client, scopes: 'bookmark_list');
$this->token = $codes['token_type'].' '.$codes['access_token'];
}
public function testCreateList(): void
{
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(0, $jsonData['items']);
$this->client->request('POST', '/api/bookmark-lists/test-list', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('test-list', $jsonData['name']);
self::assertEquals(0, $jsonData['count']);
self::assertFalse($jsonData['isDefault']);
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertEquals('test-list', $jsonData['items'][0]['name']);
self::assertEquals(0, $jsonData['items'][0]['count']);
self::assertFalse($jsonData['items'][0]['isDefault']);
}
public function testRenameList(): void
{
$dto = new BookmarkListDto();
$dto->name = 'new-test-list';
$this->client->request('POST', '/api/bookmark-lists/test-list', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->jsonRequest('PUT', '/api/bookmark-lists/test-list', parameters: $dto->jsonSerialize(), server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('new-test-list', $jsonData['name']);
self::assertEquals(0, $jsonData['count']);
self::assertFalse($jsonData['isDefault']);
$dto = new BookmarkListDto();
$dto->name = 'new-test-list2';
$dto->isDefault = true;
$this->client->jsonRequest('PUT', '/api/bookmark-lists/new-test-list', parameters: $dto->jsonSerialize(), server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('new-test-list2', $jsonData['name']);
self::assertEquals(0, $jsonData['count']);
self::assertTrue($jsonData['isDefault']);
}
public function testDeleteList(): void
{
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(0, $jsonData['items']);
$this->client->request('POST', '/api/bookmark-lists/test-list', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
$this->client->request('DELETE', '/api/bookmark-lists/test-list', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(0, $jsonData['items']);
}
public function testMakeListDefault(): void
{
$this->client->request('POST', '/api/bookmark-lists/test-list', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->jsonRequest('PUT', '/api/bookmark-lists/test-list/makeDefault', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('test-list', $jsonData['name']);
self::assertEquals(0, $jsonData['count']);
self::assertTrue($jsonData['isDefault']);
$this->client->request('GET', '/api/bookmark-lists', server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertEquals('test-list', $jsonData['items'][0]['name']);
self::assertEquals(0, $jsonData['items'][0]['count']);
self::assertTrue($jsonData['items'][0]['isDefault']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Combined/CombinedRetrieveApiCursoredTest.php
================================================
magazine = $this->getMagazineByName('acme');
$this->user = $this->getUserByUsername('user');
$this->magazineManager->subscribe($this->magazine, $this->user);
for ($i = 0; $i < 10; ++$i) {
$entry = $this->getEntryByTitle("Test Entry $i", magazine: $this->magazine);
$entry->createdAt = new \DateTimeImmutable("now - $i minutes");
$this->entityManager->persist($entry);
$this->generatedEntries[] = $entry;
++$i;
$post = $this->createPost("Test Post $i", magazine: $this->magazine);
$post->createdAt = new \DateTimeImmutable("now - $i minutes");
$this->entityManager->persist($post);
$this->generatedPosts[] = $post;
}
$this->entityManager->flush();
}
public function testCombinedAnonymous(): void
{
$this->client->request('GET', '/api/combined?perPage=2&content=all&sort=newest');
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(WebTestCase::PAGINATED_KEYS, $data);
self::assertCount(2, $data['items']);
self::assertArrayKeysMatch(WebTestCase::PAGINATION_KEYS, $data['pagination']);
self::assertEquals(5, $data['pagination']['maxPage']);
self::assertArrayKeysMatch(WebTestCase::ENTRY_RESPONSE_KEYS, $data['items'][0]['entry']);
self::assertNull($data['items'][0]['post']);
assertEquals($this->generatedEntries[0]->getId(), $data['items'][0]['entry']['entryId']);
self::assertArrayKeysMatch(WebTestCase::POST_RESPONSE_KEYS, $data['items'][1]['post']);
self::assertNull($data['items'][1]['entry']);
assertEquals($this->generatedPosts[0]->getId(), $data['items'][1]['post']['postId']);
}
public function testCombinedCursoredAnonymous(): void
{
$this->client->request('GET', '/api/combined/v2?perPage=2&sort=newest');
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
$this->assertCursorDataShape($data);
}
public function testUserCombinedCursored(): void
{
$this->client->loginUser($this->user);
self::createOAuth2PublicAuthCodeClient();
$codes = self::getPublicAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/combined/v2/subscribed?perPage=2&sort=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
$this->assertCursorDataShape($data);
}
public function testCombinedCursoredPagination(): void
{
$this->client->request('GET', '/api/combined/v2?perPage=2&sort=commented');
self::assertResponseIsSuccessful();
$data1 = self::getJsonResponse($this->client);
self::assertCount(2, $data1['items']);
self::assertArrayKeysMatch(WebTestCase::CURSOR_PAGINATION_KEYS, $data1['pagination']);
self::assertNotNull($data1['pagination']['nextCursor']);
self::assertNotNull($data1['pagination']['nextCursor2']);
self::assertNotNull($data1['pagination']['currentCursor']);
self::assertNotNull($data1['pagination']['currentCursor2']);
self::assertNull($data1['pagination']['previousCursor']);
self::assertNull($data1['pagination']['previousCursor2']);
$this->client->request('GET', '/api/combined/v2?perPage=2&sort=commented&cursor='.urlencode($data1['pagination']['nextCursor']).'&cursor2='.urlencode($data1['pagination']['nextCursor2']));
self::assertResponseIsSuccessful();
$data2 = self::getJsonResponse($this->client);
self::assertCount(2, $data2['items']);
self::assertArrayKeysMatch(WebTestCase::CURSOR_PAGINATION_KEYS, $data2['pagination']);
self::assertNotNull($data2['pagination']['nextCursor']);
self::assertNotNull($data2['pagination']['nextCursor2']);
self::assertNotNull($data2['pagination']['currentCursor']);
self::assertNotNull($data2['pagination']['currentCursor2']);
self::assertNotNull($data2['pagination']['previousCursor']);
self::assertNotNull($data2['pagination']['previousCursor2']);
$this->client->request('GET', '/api/combined/v2?perPage=2&sort=commented&cursor='.urlencode($data2['pagination']['previousCursor']).'&cursor2='.urlencode($data2['pagination']['previousCursor2']));
self::assertResponseIsSuccessful();
$data3 = self::getJsonResponse($this->client);
self::assertCount(2, $data3['items']);
self::assertArrayKeysMatch(WebTestCase::CURSOR_PAGINATION_KEYS, $data3['pagination']);
self::assertNotNull($data3['pagination']['nextCursor']);
self::assertNotNull($data3['pagination']['nextCursor2']);
self::assertNotNull($data3['pagination']['currentCursor']);
self::assertNotNull($data3['pagination']['currentCursor2']);
self::assertNull($data3['pagination']['previousCursor']);
self::assertNull($data3['pagination']['previousCursor2']);
self::assertEquals($data1['items'][0]['entry']['entryId'], $data3['items'][0]['entry']['entryId']);
self::assertEquals($data1['items'][1]['post']['postId'], $data3['items'][1]['post']['postId']);
}
private function assertCursorDataShape(array $data): void
{
self::assertArrayKeysMatch(WebTestCase::PAGINATED_KEYS, $data);
self::assertCount(2, $data['items']);
self::assertArrayKeysMatch(WebTestCase::CURSOR_PAGINATION_KEYS, $data['pagination']);
self::assertNotNull($data['pagination']['nextCursor']);
self::assertNotNull($data['pagination']['nextCursor2']);
self::assertNotNull($data['pagination']['currentCursor']);
self::assertNotNull($data['pagination']['currentCursor2']);
self::assertNull($data['pagination']['previousCursor']);
self::assertNull($data['pagination']['previousCursor2']);
self::assertArrayKeysMatch(WebTestCase::ENTRY_RESPONSE_KEYS, $data['items'][0]['entry']);
self::assertNull($data['items'][0]['post']);
assertEquals($this->generatedEntries[0]->getId(), $data['items'][0]['entry']['entryId']);
self::assertArrayKeysMatch(WebTestCase::POST_RESPONSE_KEYS, $data['items'][1]['post']);
self::assertNull($data['items'][1]['entry']);
assertEquals($this->generatedPosts[0]->getId(), $data['items'][1]['post']['postId']);
$this->client->request('GET', '/api/combined/v2?perPage=2&sort=newest&cursor='.urlencode($data['pagination']['nextCursor']));
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertCount(2, $data['items']);
self::assertArrayKeysMatch(WebTestCase::CURSOR_PAGINATION_KEYS, $data['pagination']);
self::assertNotNull($data['pagination']['nextCursor']);
self::assertNotNull($data['pagination']['nextCursor2']);
self::assertNotNull($data['pagination']['currentCursor']);
self::assertNotNull($data['pagination']['currentCursor2']);
self::assertNotNull($data['pagination']['previousCursor']);
self::assertNotNull($data['pagination']['previousCursor2']);
self::assertArrayKeysMatch(WebTestCase::ENTRY_RESPONSE_KEYS, $data['items'][0]['entry']);
self::assertNull($data['items'][0]['post']);
assertEquals($this->generatedEntries[1]->getId(), $data['items'][0]['entry']['entryId']);
self::assertArrayKeysMatch(WebTestCase::POST_RESPONSE_KEYS, $data['items'][1]['post']);
self::assertNull($data['items'][1]['entry']);
assertEquals($this->generatedPosts[1]->getId(), $data['items'][1]['post']['postId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Combined/CombinedRetrieveApiTest.php
================================================
getUserByUsername('user');
$userFollowing = $this->getUserByUsername('user2');
$user3 = $this->getUserByUsername('user3');
$magazine = $this->getMagazineByName('abc');
$this->userManager->follow($user, $userFollowing, false);
$postFollowed = $this->createPost('a post', user: $userFollowing);
$postBoosted = $this->createPost('third user post', user: $user3);
$this->createPost('unrelated post', user: $user3);
$postCommentFollowed = $this->createPostComment('a comment', $postBoosted, $userFollowing);
$postCommentBoosted = $this->createPostComment('a boosted comment', $postBoosted, $user3);
$this->createPostComment('unrelated comment', $postBoosted, $user3);
$entryFollowed = $this->createEntry('title', $magazine, body: 'an entry', user: $userFollowing);
$entryBoosted = $this->createEntry('title', $magazine, body: 'third user post', user: $user3);
$this->createEntry('title', $magazine, body: 'unrelated post', user: $user3);
$entryCommentFollowed = $this->createEntryComment('a comment', $entryBoosted, $userFollowing);
$entryCommentBoosted = $this->createEntryComment('a boosted comment', $entryBoosted, $user3);
$this->createEntryComment('unrelated comment', $entryBoosted, $user3);
$this->voteManager->upvote($postBoosted, $userFollowing);
$this->voteManager->upvote($postCommentBoosted, $userFollowing);
$this->voteManager->upvote($entryBoosted, $userFollowing);
$this->voteManager->upvote($entryCommentBoosted, $userFollowing);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/combined/subscribed?includeBoosts=true', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(8, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(8, $jsonData['pagination']['count']);
$retrievedPostIds = array_map(function ($item) {
if (null !== $item['post']) {
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $item['post']);
return $item['post']['postId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedPostIds = array_filter($retrievedPostIds, function ($item) { return null !== $item; });
sort($retrievedPostIds);
$retrievedPostCommentIds = array_map(function ($item) {
if (null !== $item['postComment']) {
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $item['postComment']);
return $item['postComment']['commentId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedPostCommentIds = array_filter($retrievedPostCommentIds, function ($item) { return null !== $item; });
sort($retrievedPostCommentIds);
$retrievedEntryIds = array_map(function ($item) {
if (null !== $item['entry']) {
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $item['entry']);
return $item['entry']['entryId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedEntryIds = array_filter($retrievedEntryIds, function ($item) { return null !== $item; });
sort($retrievedEntryIds);
$retrievedEntryCommentIds = array_map(function ($item) {
if (null !== $item['entryComment']) {
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $item['entryComment']);
return $item['entryComment']['commentId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedEntryCommentIds = array_filter($retrievedEntryCommentIds, function ($item) { return null !== $item; });
sort($retrievedEntryCommentIds);
$expectedPostIds = [$postFollowed->getId(), $postBoosted->getId()];
sort($expectedPostIds);
$expectedPostCommentIds = [$postCommentFollowed->getId(), $postCommentBoosted->getId()];
sort($expectedPostCommentIds);
$expectedEntryIds = [$entryFollowed->getId(), $entryBoosted->getId()];
sort($expectedEntryIds);
$expectedEntryCommentIds = [$entryCommentFollowed->getId(), $entryCommentBoosted->getId()];
sort($expectedEntryCommentIds);
self::assertEquals($retrievedPostIds, $expectedPostIds);
self::assertEquals($expectedPostCommentIds, $expectedPostCommentIds);
self::assertEquals($expectedEntryIds, $retrievedEntryIds);
self::assertEquals($expectedEntryCommentIds, $retrievedEntryCommentIds);
}
public function testApiHonersIncludeBoostsUserSetting(): void
{
$user = $this->getUserByUsername('user');
$userFollowing = $this->getUserByUsername('user2');
$user3 = $this->getUserByUsername('user3');
$this->userManager->follow($user, $userFollowing, false);
$this->createPost('a post', user: $userFollowing);
$postBoosted = $this->createPost('third user post', user: $user3);
$this->voteManager->upvote($postBoosted, $userFollowing);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/combined/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
$this->userRepository->find($user->getId())->showBoostsOfFollowing = true;
$this->entityManager->flush();
$this->client->request('GET', '/api/combined/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Domain/DomainBlockApiTest.php
================================================
getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$this->client->request('PUT', "/api/domain/{$domain->getId()}/block");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotBlockDomainWithoutScope()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/block", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanBlockDomain()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/block", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertTrue($jsonData['isBlockedByUser']);
// Scope not granted so subscribe flag not populated
self::assertNull($jsonData['isUserSubscribed']);
// Idempotent when called multiple times
$this->client->request('PUT', "/api/domain/{$domain->getId()}/block", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertTrue($jsonData['isBlockedByUser']);
// Scope not granted so subscribe flag not populated
self::assertNull($jsonData['isUserSubscribed']);
}
public function testApiCannotUnblockDomainAnonymous()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unblock");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnblockDomainWithoutScope()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unblock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanUnblockDomain()
{
$user = $this->getUserByUsername('JohnDoe');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$manager = $this->domainManager;
$manager->block($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unblock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertFalse($jsonData['isBlockedByUser']);
// Scope not granted so subscribe flag not populated
self::assertNull($jsonData['isUserSubscribed']);
// Idempotent when called multiple times
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unblock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertFalse($jsonData['isBlockedByUser']);
// Scope not granted so subscribe flag not populated
self::assertNull($jsonData['isUserSubscribed']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Domain/DomainRetrieveApiTest.php
================================================
getEntryByTitle('Test link to a domain', 'https://example.com');
$this->client->request('GET', '/api/domains');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('example.com', $jsonData['items'][0]['name']);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertSame(0, $jsonData['items'][0]['subscriptionsCount']);
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCanRetrieveDomains()
{
$this->getEntryByTitle('Test link to a domain', 'https://example.com');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('example.com', $jsonData['items'][0]['name']);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertSame(0, $jsonData['items'][0]['subscriptionsCount']);
// Scope not granted so subscription and block flags not populated
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCanRetrieveDomainsSubscriptionAndBlockStatus()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:subscribe domain:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('example.com', $jsonData['items'][0]['name']);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertSame(1, $jsonData['items'][0]['subscriptionsCount']);
// Scope granted so subscription and block flags populated
self::assertTrue($jsonData['items'][0]['isUserSubscribed']);
self::assertFalse($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCannotRetrieveSubscribedDomainsAnonymous()
{
$this->client->request('GET', '/api/domains/subscribed');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveSubscribedDomainsWithoutScope()
{
$this->getEntryByTitle('Test link to a second domain', 'https://example.org');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveSubscribedDomains()
{
$this->getEntryByTitle('Test link to a second domain', 'https://example.org');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertEquals('example.com', $jsonData['items'][0]['name']);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertSame(1, $jsonData['items'][0]['subscriptionsCount']);
// Scope granted so subscription flag populated
self::assertTrue($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCannotRetrieveBlockedDomainsAnonymous()
{
$this->client->request('GET', '/api/domains/blocked');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveBlockedDomainsWithoutScope()
{
$this->getEntryByTitle('Test link to a second domain', 'https://example.org');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->block($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains/blocked', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveBlockedDomains()
{
$this->getEntryByTitle('Test link to a second domain', 'https://example.org');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->block($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/domains/blocked', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertEquals('example.com', $jsonData['items'][0]['name']);
self::assertSame(1, $jsonData['items'][0]['entryCount']);
self::assertSame(0, $jsonData['items'][0]['subscriptionsCount']);
// Scope granted so block flag populated
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertTrue($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCanRetrieveDomainByIdAnonymous()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$this->client->request('GET', "/api/domain/{$domain->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveDomainById()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(1, $jsonData['subscriptionsCount']);
// Scope not granted so subscription and block flags not populated
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveDomainByIdSubscriptionAndBlockStatus()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$user = $this->getUserByUsername('JohnDoe');
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:subscribe domain:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(1, $jsonData['subscriptionsCount']);
// Scope granted so subscription and block flags populated
self::assertTrue($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Domain/DomainSubscribeApiTest.php
================================================
getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$this->client->request('PUT', "/api/domain/{$domain->getId()}/subscribe");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSubscribeToDomainWithoutScope()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/subscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSubscribeToDomain()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/subscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(1, $jsonData['subscriptionsCount']);
self::assertTrue($jsonData['isUserSubscribed']);
// Scope not granted so block flag not populated
self::assertNull($jsonData['isBlockedByUser']);
// Idempotent when called multiple times
$this->client->request('PUT', "/api/domain/{$domain->getId()}/subscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(1, $jsonData['subscriptionsCount']);
self::assertTrue($jsonData['isUserSubscribed']);
// Scope not granted so block flag not populated
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCannotUnsubscribeFromDomainAnonymous()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unsubscribe");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnsubscribeFromDomainWithoutScope()
{
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unsubscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnsubscribeFromDomain()
{
$user = $this->getUserByUsername('JohnDoe');
$domain = $this->getEntryByTitle('Test link to a domain', 'https://example.com')->domain;
$manager = $this->domainManager;
$manager->subscribe($domain, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read domain:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unsubscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertFalse($jsonData['isUserSubscribed']);
// Scope not granted so block flag not populated
self::assertNull($jsonData['isBlockedByUser']);
// Idempotent when called multiple times
$this->client->request('PUT', "/api/domain/{$domain->getId()}/unsubscribe", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(DomainRetrieveApiTest::DOMAIN_RESPONSE_KEYS, $jsonData);
self::assertEquals('example.com', $jsonData['name']);
self::assertSame(1, $jsonData['entryCount']);
self::assertSame(0, $jsonData['subscriptionsCount']);
self::assertFalse($jsonData['isUserSubscribed']);
// Scope not granted so block flag not populated
self::assertNull($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Admin/EntryChangeMagazineApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$magazine2 = $this->getMagazineByNameNoRSAKey('acme2');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/admin/entry/{$entry->getId()}/change-magazine/{$magazine2->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonAdminCannotChangeEntryMagazine(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$magazine2 = $this->getMagazineByNameNoRSAKey('acme2');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:magazine:move_entry');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/admin/entry/{$entry->getId()}/change-magazine/{$magazine2->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotChangeEntryMagazineWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$magazine2 = $this->getMagazineByNameNoRSAKey('acme2');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/admin/entry/{$entry->getId()}/change-magazine/{$magazine2->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanChangeEntryMagazine(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$magazine2 = $this->getMagazineByNameNoRSAKey('acme2');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:magazine:move_entry');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/admin/entry/{$entry->getId()}/change-magazine/{$magazine2->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine2->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Admin/EntryPurgeApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', magazine: $magazine);
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeArticleEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminCannotPurgeArticleEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPurgeArticleEntry(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotPurgeLinkEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', magazine: $magazine);
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeLinkEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminCannotPurgeLinkEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPurgeLinkEntry(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotPurgeImageEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, magazine: $magazine);
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeImageEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user', isAdmin: true);
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminCannotPurgeImageEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPurgeImageEntry(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/entry/{$entry->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/Admin/EntryCommentPurgeApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$commentRepository = $this->entryCommentRepository;
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge");
self::assertResponseStatusCodeSame(401);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCannotPurgeArticleEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiNonAdminCannotPurgeComment(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $otherUser, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCanPurgeComment(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
public function testApiCannotPurgeImageCommentAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, imageDto: $imageDto);
$commentRepository = $this->entryCommentRepository;
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge");
self::assertResponseStatusCodeSame(401);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCannotPurgeImageCommentWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user', isAdmin: true);
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, imageDto: $imageDto);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiNonAdminCannotPurgeImageComment(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, imageDto: $imageDto);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCanPurgeImageComment(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, imageDto: $imageDto);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:entry_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/DomainEntryCommentRetrieveApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$domain = $entry->domain;
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
}
public function testApiCanGetDomainEntryComments(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$domain = $entry->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
}
public function testApiCanGetDomainEntryCommentsDepth(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$nested1 = $this->createEntryComment('test comment nested 1', $entry, parent: $comment);
$nested2 = $this->createEntryComment('test comment nested 2', $entry, parent: $nested1);
$nested3 = $this->createEntryComment('test comment nested 3', $entry, parent: $nested2);
$domain = $entry->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?d=2", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(3, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertCount(1, $jsonData['items'][0]['children']);
$child = $jsonData['items'][0]['children'][0];
self::assertIsArray($child);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $child);
self::assertSame(2, $child['childCount']);
self::assertIsArray($child['children']);
self::assertCount(1, $child['children']);
self::assertIsArray($child['children'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $child);
self::assertSame(1, $child['children'][0]['childCount']);
self::assertIsArray($child['children'][0]['children']);
self::assertEmpty($child['children'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
}
public function testApiCanGetDomainEntryCommentsNewest(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$domain = $entry->domain;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetDomainEntryCommentsOldest(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$domain = $entry->domain;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetDomainEntryCommentsActive(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$domain = $entry->domain;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetDomainEntryCommentsTop(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$domain = $entry->domain;
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($this->getUserByUsername('voter1'), $first);
$favouriteManager->toggle($this->getUserByUsername('voter2'), $first);
$favouriteManager->toggle($this->getUserByUsername('voter1'), $second);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['favourites']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['favourites']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['favourites']);
}
public function testApiCanGetDomainEntryCommentsHot(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$domain = $entry->domain;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments?sort=hot", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetDomainEntryCommentsWithUserVoteStatus(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$domain = $entry->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentCreateApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments",
parameters: $comment
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateCommentWithoutScope(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateComment(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['parentId']);
}
public function testApiCannotCreateCommentReplyAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply",
parameters: $comment
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateCommentReplyWithoutScope(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateCommentReply(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertSame($entryComment->getId(), $jsonData['rootId']);
self::assertSame($entryComment->getId(), $jsonData['parentId']);
}
public function testApiCannotCreateImageCommentAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image]
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageCommentWithoutScope(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageComment(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['parentId']);
}
public function testApiCannotCreateImageCommentReplyAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image]
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageCommentReplyWithoutScope(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageCommentReply(): void
{
$imageManager = $this->imageManager;
$entry = $this->getEntryByTitle('an entry', body: 'test');
$entryComment = $this->createEntryComment('a comment', $entry);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$resultingPath = $imageManager->getFilePath($image->getFilename());
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/entry/{$entry->getId()}/comments/{$entryComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertSame($entryComment->getId(), $jsonData['rootId']);
self::assertSame($entryComment->getId(), $jsonData['parentId']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertEquals($resultingPath, $jsonData['image']['filePath']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentDeleteApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->request('DELETE', "/api/comments/{$comment->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
public function testApiCanSoftDeleteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$this->createEntryComment('test comment', $entry, $user, $comment);
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
self::assertTrue($comment->isSoftDeleted());
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentReportApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$report = [
'reason' => 'This comment breaks the rules!',
];
$this->client->jsonRequest('POST', "/api/comments/{$comment->getId()}/report", $report);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotReportCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanReportOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
$reportRepository = $this->reportRepository;
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$report = $reportRepository->findBySubject($comment);
self::assertNotNull($report);
self::assertSame('This comment breaks the rules!', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
}
public function testApiCanReportOwnComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$reportRepository = $this->reportRepository;
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$report = $reportRepository->findBySubject($comment);
self::assertNotNull($report);
self::assertSame('This comment breaks the rules!', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentRetrieveApiTest.php
================================================
getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
}
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertNull($comment['bookmarks']);
}
}
public function testApiCannotGetEntryCommentsByPreferredLangAnonymous(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
}
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?usePreferredLangs=true");
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetEntryCommentsByPreferredLang(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
$this->createEntryComment("test german parent comment {$i}", $entry, lang: 'de');
$this->createEntryComment("test dutch parent comment {$i}", $entry, lang: 'nl');
}
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$user->preferredLanguages = ['en', 'de'];
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?usePreferredLangs=true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetEntryCommentsWithLanguageAnonymous(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
$this->createEntryComment("test german parent comment {$i}", $entry, lang: 'de');
$this->createEntryComment("test dutch comment {$i}", $entry, lang: 'nl');
}
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?lang[]=en&lang[]=de");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertNull($comment['bookmarks']);
}
}
public function testApiCanGetEntryCommentsWithLanguage(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
$this->createEntryComment("test german parent comment {$i}", $entry, lang: 'de');
$this->createEntryComment("test dutch parent comment {$i}", $entry, lang: 'nl');
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?lang[]=en&lang[]=de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetEntryComments(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$this->createEntryComment("test parent comment {$i}", $entry);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetEntryCommentsWithChildren(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 5; ++$i) {
$comment = $this->createEntryComment("test parent comment {$i}", $entry);
$this->createEntryComment("test child comment {$i}", $entry, parent: $comment);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(1, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertCount(1, $comment['children']);
self::assertIsArray($comment['children'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment['children'][0]);
self::assertStringContainsString('test child comment', $comment['children'][0]['body']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetEntryCommentsLimitedDepth(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
for ($i = 0; $i < 2; ++$i) {
$comment = $this->createEntryComment("test parent comment {$i}", $entry);
$parent = $comment;
for ($j = 1; $j <= 5; ++$j) {
$parent = $this->createEntryComment("test child comment {$i} depth {$j}", $entry, parent: $parent);
}
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?d=3", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($entry->getId(), $comment['entryId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['dv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(5, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertCount(1, $comment['children']);
$depth = 0;
$current = $comment;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $current['children'][0]);
self::assertStringContainsString('test child comment', $current['children'][0]['body']);
self::assertSame(5 - ($depth + 1), $current['children'][0]['childCount']);
$current = $current['children'][0];
++$depth;
}
self::assertSame(3, $depth);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetEntryCommentByIdAnonymous(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
$comment = $this->createEntryComment('test parent comment', $entry);
$this->client->request('GET', "/api/comments/{$comment->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertEmpty($jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['bookmarks']);
}
public function testApiCanGetEntryCommentById(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
$comment = $this->createEntryComment('test parent comment', $entry);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertEmpty($jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
// No scope granted so these should be null
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetEntryCommentByIdWithDepth(): void
{
$entry = $this->getEntryByTitle('test entry', body: 'test');
$comment = $this->createEntryComment('test parent comment', $entry);
$parent = $comment;
for ($i = 0; $i < 5; ++$i) {
$parent = $this->createEntryComment('test nested reply', $entry, parent: $parent);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/comments/{$comment->getId()}?d=2", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(5, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertCount(1, $jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
// No scope granted so these should be null
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
$depth = 0;
$current = $jsonData;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $current['children'][0]);
++$depth;
$current = $current['children'][0];
}
self::assertSame(2, $depth);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentUpdateApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/comments/{$comment->getId()}", $update);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/comments/{$comment->getId()}", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/comments/{$comment->getId()}", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$parent = $comment;
for ($i = 0; $i < 5; ++$i) {
$parent = $this->createEntryComment('test reply', $entry, $user, $parent);
}
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/comments/{$comment->getId()}?d=2", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame($update['body'], $jsonData['body']);
self::assertSame($update['lang'], $jsonData['lang']);
self::assertSame($update['isAdult'], $jsonData['isAdult']);
self::assertSame(5, $jsonData['childCount']);
$depth = 0;
$current = $jsonData;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $current['children'][0]);
++$depth;
$current = $current['children'][0];
}
self::assertSame(2, $depth);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentVoteApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpvoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpvoteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(1, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(1, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
public function testApiCannotDownvoteCommentAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/-1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDownvoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDownvoteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(1, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(-1, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
public function testApiCannotRemoveVoteCommentAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $this->getUserByUsername('user'), rateLimit: false);
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/0");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRemoveVoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRemoveVoteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
public function testApiCannotFavouriteCommentAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->request('PUT', "/api/comments/{$comment->getId()}/favourite");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotFavouriteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanFavouriteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(1, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertTrue($jsonData['isFavourited']);
}
public function testApiCannotUnfavouriteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnfavouriteComment(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/EntryCommentsActivityApiTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $user, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user);
$this->client->jsonRequest('GET', "/api/comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
}
public function testUpvotes()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $author, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $author);
$this->favouriteManager->toggle($user1, $comment);
$this->favouriteManager->toggle($user2, $comment);
$this->client->jsonRequest('GET', "/api/comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['upvotes']);
self::assertTrue(array_all($jsonData['upvotes'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['upvotes']));
}
public function testBoosts()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $author, magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $author);
$this->voteManager->upvote($comment, $user1);
$this->voteManager->upvote($comment, $user2);
$this->client->jsonRequest('GET', "/api/comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['boosts']);
self::assertTrue(array_all($jsonData['boosts'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['boosts']));
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/Moderate/EntryCommentSetAdultApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/true");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSetCommentAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('user2');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotSetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertTrue($jsonData['isAdult']);
}
public function testApiCannotUnsetCommentAdultAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/false");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnsetCommentAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('user2');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotUnsetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnsetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
$commentRepository = $this->entryCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertFalse($jsonData['isAdult']);
$comment = $commentRepository->find($comment->getId());
self::assertFalse($comment->isAdult);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/Moderate/EntryCommentSetLanguageApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/de");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSetCommentLanguageWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('user2');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotSetCommentLanguage(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetCommentLanguage(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('de', $jsonData['lang']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/Moderate/EntryCommentTrashApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/trash");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotTrashCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('user2');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotTrashComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanTrashComment(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('trashed', $jsonData['visibility']);
}
public function testApiCannotRestoreCommentAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry);
$entryCommentManager = $this->entryCommentManager;
$entryCommentManager->trash($this->getUserByUsername('user'), $comment);
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/restore");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRestoreCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
$entryCommentManager = $this->entryCommentManager;
$entryCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotRestoreComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$comment = $this->createEntryComment('test comment', $entry, $user2);
$entryCommentManager = $this->entryCommentManager;
$entryCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRestoreComment(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$entry = $this->getEntryByTitle('an entry', body: 'test', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entryCommentManager = $this->entryCommentManager;
$entryCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('visible', $jsonData['visibility']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Comment/UserEntryCommentRetrieveApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$user = $entry->user;
$this->client->request('GET', "/api/users/{$user->getId()}/comments");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
}
public function testApiCanGetUserEntryComments(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$user = $entry->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
}
public function testApiCanGetUserEntryCommentsDepth(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$nested1 = $this->createEntryComment('test comment nested 1', $entry, parent: $comment);
$nested2 = $this->createEntryComment('test comment nested 2', $entry, parent: $nested1);
$nested3 = $this->createEntryComment('test comment nested 3', $entry, parent: $nested2);
$user = $entry->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?d=2", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(4, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $comment);
self::assertTrue(\count($comment['children']) <= 1);
$depth = 0;
$current = $comment;
while (\count($current['children']) > 0) {
++$depth;
$current = $current['children'][0];
self::assertIsArray($current);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $current);
}
self::assertTrue($depth <= 2);
}
}
public function testApiCanGetUserEntryCommentsNewest(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$user = $entry->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserEntryCommentsOldest(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$user = $entry->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserEntryCommentsActive(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$user = $entry->user;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserEntryCommentsTop(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$user = $entry->user;
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($this->getUserByUsername('voter1'), $first);
$favouriteManager->toggle($this->getUserByUsername('voter2'), $first);
$favouriteManager->toggle($this->getUserByUsername('voter1'), $second);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['favourites']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['favourites']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['favourites']);
}
public function testApiCanGetUserEntryCommentsHot(): void
{
$entry = $this->getEntryByTitle('entry', url: 'https://google.com');
$first = $this->createEntryComment('first', $entry);
$second = $this->createEntryComment('second', $entry);
$third = $this->createEntryComment('third', $entry);
$user = $entry->user;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments?sort=hot", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetUserEntryCommentsWithUserVoteStatus(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$comment = $this->createEntryComment('test comment', $entry);
$user = $entry->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/DomainEntryRetrieveApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$domain = $entry->domain;
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetDomainEntries(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$domain = $entry->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetDomainEntriesNewest(): void
{
$first = $this->getEntryByTitle('first', url: 'https://google.com');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$domain = $first->domain;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetDomainEntriesOldest(): void
{
$first = $this->getEntryByTitle('first', url: 'https://google.com');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$domain = $first->domain;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetDomainEntriesCommented(): void
{
$first = $this->getEntryByTitle('first', url: 'https://google.com');
$this->createEntryComment('comment 1', $first);
$this->createEntryComment('comment 2', $first);
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$this->createEntryComment('comment 1', $second);
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$domain = $first->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries?sort=commented", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['numComments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['numComments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['numComments']);
}
public function testApiCanGetDomainEntriesActive(): void
{
$first = $this->getEntryByTitle('first', url: 'https://google.com');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$domain = $first->domain;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetDomainEntriesTop(): void
{
$first = $this->getEntryByTitle('first', url: 'https://google.com');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$domain = $first->domain;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetDomainEntriesWithUserVoteStatus(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$domain = $entry->domain;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/domain/{$domain->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertIsArray($jsonData['items'][0]['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]['domain']);
self::assertEquals('https://google.com', $jsonData['items'][0]['url']);
self::assertNull($jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertEquals('another-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntriesActivityApiTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $user, magazine: $magazine);
$this->client->jsonRequest('GET', "/api/entry/{$entry->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
}
public function testUpvotes()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $author, magazine: $magazine);
$this->favouriteManager->toggle($user1, $entry);
$this->favouriteManager->toggle($user2, $entry);
$this->client->jsonRequest('GET', "/api/entry/{$entry->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['upvotes']);
self::assertTrue(array_all($jsonData['upvotes'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['upvotes']));
}
public function testBoosts()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for activites', user: $author, magazine: $magazine);
$this->voteManager->upvote($entry, $user1);
$this->voteManager->upvote($entry, $user2);
$this->client->jsonRequest('GET', "/api/entry/{$entry->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['boosts']);
self::assertTrue(array_all($jsonData['boosts'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['boosts']));
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryCreateApiNewTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateArticleEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotCreateEntryWithoutTitle(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'body' => 'This has no title',
'url' => 'https://google.com',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCanCreateArticleEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('This is an article', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateLinkEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'url' => 'https://google.com',
'body' => 'google',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateLinkEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'url' => 'https://google.com',
'body' => 'google',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateLinkEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'url' => 'https://google.com',
'body' => 'This is a link',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertIsArray($jsonData['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['domain']);
self::assertEquals('https://google.com', $jsonData['url']);
self::assertEquals('This is a link', $jsonData['body']);
if (null !== $jsonData['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCanCreateLinkWithImageEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'url' => 'https://google.com',
'body' => 'This is a link',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/entries",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token],
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertIsArray($jsonData['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['domain']);
self::assertEquals('https://google.com', $jsonData['url']);
self::assertEquals('This is a link', $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateImageEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/entries",
parameters: $entryRequest, files: ['uploadImage' => $image],
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/entries",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/entries",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertNull($jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('It\'s kibby!', $jsonData['image']['altText']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCanCreateImageWithBodyEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'body' => 'Isn\'t it a cute picture?',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/entries",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('Isn\'t it a cute picture?', $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('It\'s kibby!', $jsonData['image']['altText']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateEntryWithoutMagazine(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$invalidId = $magazine->getId() + 1;
$entryRequest = [
'title' => 'No Url/Body Thread',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$invalidId}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCannotCreateEntryWithoutUrlBodyOrImage(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Url/Body Thread',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/entries", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryCreateApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/article", parameters: $entryRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateArticleEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/article", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateArticleEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'body' => 'This is an article',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/article", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('This is an article', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateLinkEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'url' => 'https://google.com',
'body' => 'google',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/link", parameters: $entryRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateLinkEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'url' => 'https://google.com',
'body' => 'google',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/link", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateLinkEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'url' => 'https://google.com',
'body' => 'This is a link',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/link", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertIsArray($jsonData['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['domain']);
self::assertEquals('https://google.com', $jsonData['url']);
self::assertEquals('This is a link', $jsonData['body']);
if (null !== $jsonData['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateImageEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Anonymous Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/image",
parameters: $entryRequest, files: ['uploadImage' => $image],
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Scope Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/image",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/image",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertNull($jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('It\'s kibby!', $jsonData['image']['altText']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCanCreateImageEntryWithBody(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'Test Thread',
'alt' => 'It\'s kibby!',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
'body' => 'body text',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/image",
parameters: $entryRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['entryId']);
self::assertEquals('Test Thread', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('body text', $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('It\'s kibby!', $jsonData['image']['altText']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Test-Thread', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotCreateEntryWithoutMagazine(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$invalidId = $magazine->getId() + 1;
$entryRequest = [
'title' => 'No Url/Body Thread',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$invalidId}/article", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
$this->client->jsonRequest('POST', "/api/magazine/{$invalidId}/link", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
$this->client->request('POST', "/api/magazine/{$invalidId}/image", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCannotCreateEntryWithoutUrlBodyOrImage(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entryRequest = [
'title' => 'No Url/Body Thread',
'tags' => ['test'],
'isOc' => false,
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/article", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/link", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/image", parameters: $entryRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryDeleteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', magazine: $magazine);
$this->client->request('DELETE', "/api/entry/{$entry->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteArticleEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersArticleEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteArticleEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotDeleteLinkEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com');
$this->client->request('DELETE', "/api/entry/{$entry->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteLinkEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersLinkEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteLinkEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotDeleteImageEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, magazine: $magazine);
$this->client->request('DELETE', "/api/entry/{$entry->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteImageEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteOtherUsersImageEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteImageEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryFavouriteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/favourite");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotFavouriteEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanFavouriteEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(1, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertTrue($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryReportApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for report', magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
$this->client->jsonRequest('POST', "/api/entry/{$entry->getId()}/report", $reportRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotReportEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for report', user: $user, magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/entry/{$entry->getId()}/report", $reportRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanReportEntry(): void
{
$user = $this->getUserByUsername('user');
$otherUser = $this->getUserByUsername('somebody');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for report', user: $otherUser, magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
$magazineRepository = $this->magazineRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/entry/{$entry->getId()}/report", $reportRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$magazine = $magazineRepository->find($magazine->getId());
$reports = $magazineRepository->findReports($magazine);
self::assertSame(1, $reports->count());
/** @var Report $report */
$report = $reports->getCurrentPageResults()[0];
self::assertEquals('Test reporting', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
self::assertSame($otherUser->getId(), $report->reported->getId());
self::assertSame($entry->getId(), $report->getSubject()->getId());
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryRetrieveApiTest.php
================================================
client->request('GET', '/api/entries/subscribed');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetSubscribedEntriesWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'write');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetSubscribedEntries(): void
{
$user = $this->getUserByUsername('user');
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag', $user);
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertIsArray($jsonData['items'][0]['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]['domain']);
self::assertEquals('https://google.com', $jsonData['items'][0]['url']);
self::assertNull($jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertEquals('another-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCannotGetModeratedEntriesAnonymous(): void
{
$this->client->request('GET', '/api/entries/moderated');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetModeratedEntriesWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetModeratedEntries(): void
{
$user = $this->getUserByUsername('user');
$this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag', $user);
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertIsArray($jsonData['items'][0]['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]['domain']);
self::assertEquals('https://google.com', $jsonData['items'][0]['url']);
self::assertNull($jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertEquals('another-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCannotGetFavouritedEntriesAnonymous(): void
{
$this->client->request('GET', '/api/entries/favourited');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetFavouritedEntriesWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/favourited', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetFavouritedEntries(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('an entry', body: 'test');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $entry);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries/favourited', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(1, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertTrue($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCanGetEntriesAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
// Check that pinned entries don't get pinned to the top of the instance, just the magazine
$entryManager = $this->entryManager;
$entryManager->pin($second, null);
$this->client->request('GET', '/api/entries');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
self::assertNull($jsonData['items'][0]['bookmarks']);
}
public function testApiCanGetEntries(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
}
public function testApiCanGetEntriesWithLanguageAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine, lang: 'de');
$this->getEntryByTitle('a dutch entry', body: 'some body', magazine: $magazine, lang: 'nl');
// Check that pinned entries don't get pinned to the top of the instance, just the magazine
$entryManager = $this->entryManager;
$entryManager->pin($second, null);
$this->client->request('GET', '/api/entries?lang[]=en&lang[]=de');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertEquals('de', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
}
public function testApiCanGetEntriesWithLanguage(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine, lang: 'de');
$this->getEntryByTitle('a dutch entry', body: 'some body', magazine: $magazine, lang: 'nl');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?lang[]=en&lang[]=de', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertEquals('de', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
}
public function testApiCannotGetEntriesByPreferredLangAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
// Check that pinned entries don't get pinned to the top of the instance, just the magazine
$entryManager = $this->entryManager;
$entryManager->pin($second, null);
$this->client->request('GET', '/api/entries?usePreferredLangs=true');
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetEntriesByPreferredLang(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$this->getEntryByTitle('German entry', body: 'Some body', lang: 'de');
$user = $this->getUserByUsername('user');
$user->preferredLanguages = ['en'];
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?usePreferredLangs=true', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertIsArray($jsonData['items'][0]['badges']);
self::assertEmpty($jsonData['items'][0]['badges']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertEquals('en', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
}
public function testApiCanGetEntriesNewest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetEntriesOldest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=oldest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetEntriesCommented(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$this->createEntryComment('comment 1', $first);
$this->createEntryComment('comment 2', $first);
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$this->createEntryComment('comment 1', $second);
$third = $this->getEntryByTitle('third', url: 'https://google.com');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=commented', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['numComments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['numComments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['numComments']);
}
public function testApiCanGetEntriesActive(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=active', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetEntriesTop(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=top', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetEntriesWithUserVoteStatus(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('an entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['domain']);
self::assertNull($jsonData['items'][0]['url']);
self::assertEquals('test', $jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(1, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['items'][0]['type']);
self::assertEquals('an-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertSame(0, $jsonData['items'][1]['numComments']);
}
public function testApiCanGetEntryByIdAnonymous(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->client->request('GET', "/api/entry/{$entry->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals('an entry', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('test', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('an-entry', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertNull($jsonData['bookmarks']);
}
public function testApiCanGetEntryById(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals('an entry', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('test', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('an-entry', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetEntryByIdWithUserVoteStatus(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/entry/{$entry->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals('an entry', $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals('test', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertFalse($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('an-entry', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetEntriesLocal(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', body: 'test2');
$second->apId = 'https://some.url';
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?federation=local', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertTrue($jsonData['items'][0]['isAuthorModeratorInMagazine']);
}
public function testApiCanGetEntriesFederated(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', body: 'test2');
$second->apId = 'https://some.url';
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?federation=federated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($second->getId(), $jsonData['items'][0]['entryId']);
self::assertTrue($jsonData['items'][0]['isAuthorModeratorInMagazine']);
}
public function testApiGetAuthorNotModerator(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
sleep(1);
$second = $this->getEntryByTitle('second', body: 'test2', user: $this->getUserByUsername('Jane Doe'));
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/entries?sort=oldest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertTrue($jsonData['items'][0]['isAuthorModeratorInMagazine']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertFalse($jsonData['items'][1]['isAuthorModeratorInMagazine']);
}
/**
* This function tests that the collection endpoint does not contain crosspost information,
* but fetching a single entry does.
*/
public function testApiContainsCrosspostInformation(): void
{
$magazine1 = $this->getMagazineByName('acme');
$entry1 = $this->getEntryByTitle('first URL', url: 'https://joinmbin.org', magazine: $magazine1);
sleep(1);
$magazine2 = $this->getMagazineByName('acme2');
$entry2 = $this->getEntryByTitle('second URL', url: 'https://joinmbin.org', magazine: $magazine2);
$this->entityManager->persist($entry1);
$this->entityManager->persist($entry2);
$this->entityManager->flush();
$this->client->request('GET', '/api/entries?sort=oldest');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry1->getId(), $jsonData['items'][0]['entryId']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($entry2->getId(), $jsonData['items'][1]['entryId']);
self::assertNull($jsonData['items'][1]['crosspostedEntries']);
$this->client->request('GET', '/api/entry/'.$entry1->getId());
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['crosspostedEntries']);
self::assertCount(1, $jsonData['crosspostedEntries']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['crosspostedEntries'][0]);
self::assertSame($entry2->getId(), $jsonData['crosspostedEntries'][0]['entryId']);
$this->client->request('GET', '/api/entry/'.$entry2->getId());
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['crosspostedEntries']);
self::assertCount(1, $jsonData['crosspostedEntries']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['crosspostedEntries'][0]);
self::assertSame($entry1->getId(), $jsonData['crosspostedEntries'][0]['entryId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryUpdateApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for update', magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateArticleEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for update', user: $user, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersArticleEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for update', user: $otherUser, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateArticleEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for update', user: $user, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($updateRequest['title'], $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($updateRequest['body'], $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($updateRequest['lang'], $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame($updateRequest['tags'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isOc']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['editedAt'], 'editedAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('Updated-title', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUpdateLinkEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateLinkEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersLinkEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $otherUser, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateLinkEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test link', url: 'https://google.com', user: $user, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($updateRequest['title'], $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertIsArray($jsonData['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['domain']);
self::assertEquals('https://google.com', $jsonData['url']);
self::assertEquals($updateRequest['body'], $jsonData['body']);
if (null !== $jsonData['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals($updateRequest['lang'], $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame($updateRequest['tags'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isOc']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['editedAt'], 'editedAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['type']);
self::assertEquals('Updated-title', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUpdateImageEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateImageEntryWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateOtherUsersImageEntry(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $otherUser, magazine: $magazine);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanUpdateImageEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test image', image: $imageDto, user: $user, magazine: $magazine);
self::assertNotNull($imageDto->id);
self::assertNotNull($entry->image);
self::assertNotNull($entry->image->getId());
self::assertSame($imageDto->id, $entry->image->getId());
self::assertSame($imageDto->filePath, $entry->image->filePath);
$updateRequest = [
'title' => 'Updated title',
'tags' => [
'edit',
],
'isOc' => true,
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($updateRequest['title'], $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($updateRequest['body'], $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($imageDto->filePath, $jsonData['image']['filePath']);
self::assertEquals($updateRequest['lang'], $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame($updateRequest['tags'], $jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isOc']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['editedAt'], 'editedAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('image', $jsonData['type']);
self::assertEquals('Updated-title', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/EntryVoteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpvoteEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpvoteEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(1, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertFalse($jsonData['isFavourited']);
self::assertSame(1, $jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotDownvoteEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/-1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDownvoteEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDownvoteEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(1, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertFalse($jsonData['isFavourited']);
self::assertSame(-1, $jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotClearVoteEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/0");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotClearVoteEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
$voteManager = $this->voteManager;
$voteManager->vote(1, $entry, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanClearVoteEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for upvote', user: $user, magazine: $magazine);
$voteManager = $this->voteManager;
$voteManager->vote(1, $entry, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read entry:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/entry/{$entry->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertFalse($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/MagazineEntryRetrieveApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetMagazineEntries(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetMagazineEntriesPinnedFirst(): void
{
$voteManager = $this->voteManager;
$entryManager = $this->entryManager;
$voter = $this->getUserByUsername('voter');
$first = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $first);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
// Upvote and comment on $second so it should come first, but then pin $third so it actually comes first
$voteManager->vote(1, $second, $voter, rateLimit: false);
$this->createEntryComment('test', $second, $voter);
$third = $this->getEntryByTitle('a pinned entry', url: 'https://google.com', magazine: $magazine);
$entryManager->pin($third, null);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('a pinned entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertTrue($jsonData['items'][0]['isPinned']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another entry', $jsonData['items'][1]['title']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('link', $jsonData['items'][1]['type']);
self::assertSame(1, $jsonData['items'][1]['numComments']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertFalse($jsonData['items'][1]['isPinned']);
self::assertNull($jsonData['items'][1]['crosspostedEntries']);
}
public function testApiCanGetMagazineEntriesNewest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$magazine = $first->magazine;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetMagazineEntriesOldest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$magazine = $first->magazine;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetMagazineEntriesCommented(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$this->createEntryComment('comment 1', $first);
$this->createEntryComment('comment 2', $first);
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$this->createEntryComment('comment 1', $second);
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$magazine = $first->magazine;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries?sort=commented", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['numComments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['numComments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['numComments']);
}
public function testApiCanGetMagazineEntriesActive(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$magazine = $first->magazine;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetMagazineEntriesTop(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$magazine = $first->magazine;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetMagazineEntriesWithUserVoteStatus(): void
{
$first = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $first);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertIsArray($jsonData['items'][0]['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]['domain']);
self::assertEquals('https://google.com', $jsonData['items'][0]['url']);
self::assertNull($jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertEquals('another-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Moderate/EntryLockApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorNonAuthorCannotLockEntry(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user2, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotLockEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanLockEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertTrue($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiAuthorNonModeratorCanLockEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertTrue($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUnlockEntryAnonymous(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->toggleLock($entry, $user);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorNonAuthorCannotUnlockEntry(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user2, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->pin($entry, null);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnlockEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->toggleLock($entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnlockEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->toggleLock($entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertFalse($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiAuthorNonModeratorCanUnlockEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->toggleLock($entry, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertFalse($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Moderate/EntryPinApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotPinEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotPinEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPinEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertTrue($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUnpinEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->pin($entry, null);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotUnpinEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->pin($entry, null);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnpinEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->pin($entry, null);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnpinEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->pin($entry, null);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Moderate/EntrySetAdultApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/true");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetEntryAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetEntryAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetEntryAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotSetEntryNotAdultAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$entityManager = $this->entityManager;
$entry->isAdult = true;
$entityManager->persist($entry);
$entityManager->flush();
$this->client->request('PUT', "/api/moderate/entry/{$entry->getId()}/adult/false");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetEntryNotAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entityManager = $this->entityManager;
$entry->isAdult = true;
$entityManager->persist($entry);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetEntryNotAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entityManager = $this->entityManager;
$entry->isAdult = true;
$entityManager->persist($entry);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetEntryNotAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$entry->isAdult = true;
$entityManager->persist($entry);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Moderate/EntrySetLanguageApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/de");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetEntryLanguage(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetEntryLanguageWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetEntryLanguageInvalid(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/fake", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/ac", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/aaa", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/a", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCanSetEntryLanguage(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('de', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCanSetEntryLanguage3Letter(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/elx", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('elx', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/Moderate/EntryTrashApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/trash");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotTrashEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotTrashEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanTrashEntry(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('trashed', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotRestoreEntryAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->trash($user, $entry);
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/restore");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotRestoreEntry(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->trash($user, $entry);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRestoreEntryWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$entryManager = $this->entryManager;
$entryManager->trash($user, $entry);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRestoreEntry(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$entry = $this->getEntryByTitle('test article', body: 'test for favourite', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entryManager = $this->entryManager;
$entryManager->trash($user, $entry);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:entry:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/entry/{$entry->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData);
self::assertSame($entry->getId(), $jsonData['entryId']);
self::assertEquals($entry->title, $jsonData['title']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['domain']);
self::assertNull($jsonData['url']);
self::assertEquals($entry->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($entry->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertIsArray($jsonData['badges']);
self::assertEmpty($jsonData['badges']);
self::assertSame(0, $jsonData['numComments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isOc']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('article', $jsonData['type']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Entry/UserEntryRetrieveApiTest.php
================================================
getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$otherUser = $this->getUserByUsername('somebody');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine, user: $otherUser);
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetUserEntries(): void
{
$entry = $this->getEntryByTitle('an entry', body: 'test');
$this->createEntryComment('up the ranking', $entry);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$otherUser = $this->getUserByUsername('somebody');
$this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine, user: $otherUser);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
public function testApiCanGetUserEntriesNewest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$otherUser = $first->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetUserEntriesOldest(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$otherUser = $first->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetUserEntriesCommented(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$this->createEntryComment('comment 1', $first);
$this->createEntryComment('comment 2', $first);
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$this->createEntryComment('comment 1', $second);
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$otherUser = $first->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries?sort=commented", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['numComments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['numComments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['numComments']);
}
public function testApiCanGetUserEntriesActive(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$otherUser = $first->user;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['entryId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['entryId']);
}
public function testApiCanGetUserEntriesTop(): void
{
$first = $this->getEntryByTitle('first', body: 'test');
$second = $this->getEntryByTitle('second', url: 'https://google.com');
$third = $this->getEntryByTitle('third', url: 'https://google.com');
$otherUser = $first->user;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['entryId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['entryId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['entryId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetUserEntriesWithUserVoteStatus(): void
{
$this->getEntryByTitle('an entry', body: 'test');
$otherUser = $this->getUserByUsername('somebody');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$entry = $this->getEntryByTitle('another entry', url: 'https://google.com', magazine: $magazine, user: $otherUser);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/entries", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals('another entry', $jsonData['items'][0]['title']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertIsArray($jsonData['items'][0]['domain']);
self::assertArrayKeysMatch(self::DOMAIN_RESPONSE_KEYS, $jsonData['items'][0]['domain']);
self::assertEquals('https://google.com', $jsonData['items'][0]['url']);
self::assertNull($jsonData['items'][0]['body']);
if (null !== $jsonData['items'][0]['image']) {
self::assertStringContainsString('google.com', parse_url($jsonData['items'][0]['image']['sourceUrl'], PHP_URL_HOST));
}
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['numComments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isOc']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('link', $jsonData['items'][0]['type']);
self::assertEquals('another-entry', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/Admin/InstanceFederationUpdateApiTest.php
================================================
client->request('PUT', '/api/defederated');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceFederationWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/defederated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceFederationWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/defederated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceFederation(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:federation:update');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/defederated', ['instances' => ['bad-instance.com']], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(['instances'], $jsonData);
self::assertSame(['bad-instance.com'], $jsonData['instances']);
}
public function testApiCanClearInstanceFederation(): void
{
$this->instanceManager->setBannedInstances(['defederated.social', 'evil.social']);
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:federation:update');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/defederated', ['instances' => []], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(['instances'], $jsonData);
self::assertEmpty($jsonData['instances']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/Admin/InstancePagesUpdateApiTest.php
================================================
client->request('PUT', '/api/instance/about');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceAboutPageWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/about', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceAboutPageWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/about', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceAboutPage(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:information:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/instance/about', ['body' => 'about page'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(InstanceDetailsApiTest::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals('about page', $jsonData['about']);
}
public function testApiCannotUpdateInstanceContactPageAnonymous(): void
{
$this->client->request('PUT', '/api/instance/contact');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceContactPageWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/contact', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceContactPageWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/contact', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceContactPage(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:information:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/instance/contact', ['body' => 'contact page'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(InstanceDetailsApiTest::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals('contact page', $jsonData['contact']);
}
public function testApiCannotUpdateInstanceFAQPageAnonymous(): void
{
$this->client->request('PUT', '/api/instance/faq');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceFAQPageWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/faq', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceFAQPageWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/faq', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceFAQPage(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:information:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/instance/faq', ['body' => 'faq page'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(InstanceDetailsApiTest::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals('faq page', $jsonData['faq']);
}
public function testApiCannotUpdateInstancePrivacyPolicyPageAnonymous(): void
{
$this->client->request('PUT', '/api/instance/privacyPolicy');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstancePrivacyPolicyPageWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/privacyPolicy', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstancePrivacyPolicyPageWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/privacyPolicy', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstancePrivacyPolicyPage(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:information:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/instance/privacyPolicy', ['body' => 'privacyPolicy page'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(InstanceDetailsApiTest::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals('privacyPolicy page', $jsonData['privacyPolicy']);
}
public function testApiCannotUpdateInstanceTermsPageAnonymous(): void
{
$this->client->request('PUT', '/api/instance/terms');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceTermsPageWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/terms', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceTermsPageWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/terms', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceTermsPage(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:information:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', '/api/instance/terms', ['body' => 'terms page'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(InstanceDetailsApiTest::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals('terms page', $jsonData['terms']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/Admin/InstanceSettingsRetrieveApiTest.php
================================================
client->request('GET', '/api/instance/settings');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveInstanceSettingsWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/instance/settings', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveInstanceSettingsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/instance/settings', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveInstanceSettings(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:settings:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/instance/settings', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_SETTINGS_RESPONSE_KEYS, $jsonData);
foreach ($jsonData as $key => $value) {
self::assertNotNull($value, "$key was null!");
}
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/Admin/InstanceSettingsUpdateApiTest.php
================================================
client->request('PUT', '/api/instance/settings');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateInstanceSettingsWithoutAdmin(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/settings', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateInstanceSettingsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/instance/settings', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateInstanceSettings(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:instance:settings:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$settings = [
'KBIN_DOMAIN' => 'kbinupdated.test',
'KBIN_TITLE' => 'updated title',
'KBIN_META_TITLE' => 'meta title',
'KBIN_META_KEYWORDS' => 'this, is, a, test',
'KBIN_META_DESCRIPTION' => 'Testing out the API',
'KBIN_DEFAULT_LANG' => 'de',
'KBIN_CONTACT_EMAIL' => 'test@kbinupdated.test',
'KBIN_SENDER_EMAIL' => 'noreply@kbinupdated.test',
'MBIN_DEFAULT_THEME' => 'dark',
'KBIN_JS_ENABLED' => true,
'KBIN_FEDERATION_ENABLED' => true,
'KBIN_REGISTRATIONS_ENABLED' => false,
'KBIN_HEADER_LOGO' => true,
'KBIN_CAPTCHA_ENABLED' => true,
'KBIN_MERCURE_ENABLED' => false,
'KBIN_FEDERATION_PAGE_ENABLED' => false,
'KBIN_ADMIN_ONLY_OAUTH_CLIENTS' => true,
'MBIN_PRIVATE_INSTANCE' => true,
'KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN' => false,
'MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY' => false,
'MBIN_SIDEBAR_SECTIONS_USERS_LOCAL_ONLY' => false,
'MBIN_SSO_REGISTRATIONS_ENABLED' => true,
'MBIN_RESTRICT_MAGAZINE_CREATION' => false,
'MBIN_DOWNVOTES_MODE' => DownvotesMode::Enabled->value,
'MBIN_SSO_ONLY_MODE' => false,
'MBIN_SSO_SHOW_FIRST' => false,
'MBIN_NEW_USERS_NEED_APPROVAL' => false,
'MBIN_USE_FEDERATION_ALLOW_LIST' => false,
];
$this->client->jsonRequest('PUT', '/api/instance/settings', $settings, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_SETTINGS_RESPONSE_KEYS, $jsonData);
foreach ($jsonData as $key => $value) {
self::assertEquals($settings[$key], $value, "$key did not match!");
}
$settings = [
'KBIN_DOMAIN' => 'kbin.test',
'KBIN_TITLE' => 'updated title',
'KBIN_META_TITLE' => 'meta title',
'KBIN_META_KEYWORDS' => 'this, is, a, test',
'KBIN_META_DESCRIPTION' => 'Testing out the API',
'KBIN_DEFAULT_LANG' => 'en',
'KBIN_CONTACT_EMAIL' => 'test@kbinupdated.test',
'KBIN_SENDER_EMAIL' => 'noreply@kbinupdated.test',
'MBIN_DEFAULT_THEME' => 'light',
'KBIN_JS_ENABLED' => false,
'KBIN_FEDERATION_ENABLED' => false,
'KBIN_REGISTRATIONS_ENABLED' => true,
'KBIN_HEADER_LOGO' => false,
'KBIN_CAPTCHA_ENABLED' => false,
'KBIN_MERCURE_ENABLED' => true,
'KBIN_FEDERATION_PAGE_ENABLED' => true,
'KBIN_ADMIN_ONLY_OAUTH_CLIENTS' => false,
'MBIN_PRIVATE_INSTANCE' => false,
'KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN' => true,
'MBIN_SIDEBAR_SECTIONS_RANDOM_LOCAL_ONLY' => true,
'MBIN_SIDEBAR_SECTIONS_USERS_LOCAL_ONLY' => true,
'MBIN_SSO_REGISTRATIONS_ENABLED' => false,
'MBIN_RESTRICT_MAGAZINE_CREATION' => true,
'MBIN_DOWNVOTES_MODE' => DownvotesMode::Hidden->value,
'MBIN_SSO_ONLY_MODE' => true,
'MBIN_SSO_SHOW_FIRST' => true,
'MBIN_NEW_USERS_NEED_APPROVAL' => false,
'MBIN_USE_FEDERATION_ALLOW_LIST' => false,
];
$this->client->jsonRequest('PUT', '/api/instance/settings', $settings, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_SETTINGS_RESPONSE_KEYS, $jsonData);
foreach ($jsonData as $key => $value) {
self::assertEquals($settings[$key], $value, "$key did not match!");
}
}
protected function tearDown(): void
{
parent::tearDown();
SettingsManager::resetDto();
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/InstanceDetailsApiTest.php
================================================
createInstancePages();
$this->client->request('GET', '/api/instance');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals($site->about, $jsonData['about']);
self::assertEquals($site->contact, $jsonData['contact']);
self::assertEquals($site->faq, $jsonData['faq']);
self::assertEquals($site->privacyPolicy, $jsonData['privacyPolicy']);
self::assertEquals($site->terms, $jsonData['terms']);
}
public function testApiCanRetrieveInstanceDetails(): void
{
$site = $this->createInstancePages();
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/instance', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_PAGE_RESPONSE_KEYS, $jsonData);
self::assertEquals($site->about, $jsonData['about']);
self::assertEquals($site->contact, $jsonData['contact']);
self::assertEquals($site->faq, $jsonData['faq']);
self::assertEquals($site->privacyPolicy, $jsonData['privacyPolicy']);
self::assertEquals($site->terms, $jsonData['terms']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/InstanceFederationApiTest.php
================================================
instanceManager->setBannedInstances([]);
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/defederated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_DEFEDERATED_RESPONSE_KEYS, $jsonData);
self::assertSame([], $jsonData['instances']);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanRetrieveInstanceDefederationAnonymous(): void
{
$this->instanceManager->setBannedInstances(['defederated.social']);
$this->client->request('GET', '/api/defederated');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_DEFEDERATED_RESPONSE_KEYS, $jsonData);
self::assertSame(['defederated.social'], $jsonData['instances']);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanRetrieveInstanceDefederation(): void
{
$this->instanceManager->setBannedInstances(['defederated.social', 'evil.social']);
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/defederated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INSTANCE_DEFEDERATED_RESPONSE_KEYS, $jsonData);
self::assertSame(['defederated.social', 'evil.social'], $jsonData['instances']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/InstanceModlogApiTest.php
================================================
createModlogMessages();
$this->client->request('GET', '/api/modlog');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
$magazine = $this->getMagazineByName('acme');
$moderator = $magazine->getOwner();
$this->validateModlog($jsonData, $magazine, $moderator);
}
public function testApiCanRetrieveModlogAnonymousWithTypeFilter(): void
{
$this->createModlogMessages();
$this->client->request('GET', '/api/modlog?types[]='.MagazineLog::CHOICES[0].'&types[]='.MagazineLog::CHOICES[1]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
}
public function testApiCanRetrieveModlog(): void
{
$this->createModlogMessages();
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/modlog', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
$magazine = $this->getMagazineByName('acme');
$moderator = $magazine->getOwner();
$this->validateModlog($jsonData, $magazine, $moderator);
}
}
================================================
FILE: tests/Functional/Controller/Api/Instance/InstanceRetrieveInfoApiTest.php
================================================
getUserByUsername('admin', isAdmin: true);
$this->getUserByUsername('moderator', isModerator: true);
$this->client->request('GET', '/api/info');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::INFO_KEYS, $jsonData);
self::assertIsString($jsonData['softwareName']);
self::assertIsString($jsonData['softwareVersion']);
self::assertIsString($jsonData['softwareRepository']);
self::assertIsString($jsonData['websiteDomain']);
self::assertIsString($jsonData['websiteContactEmail']);
self::assertIsString($jsonData['websiteTitle']);
self::assertIsBool($jsonData['websiteOpenRegistrations']);
self::assertIsBool($jsonData['websiteFederationEnabled']);
self::assertIsString($jsonData['websiteDefaultLang']);
self::assertIsArray($jsonData['instanceAdmins']);
self::assertIsArray($jsonData['instanceModerators']);
self::assertNotEmpty($jsonData['instanceAdmins']);
self::assertNotEmpty($jsonData['instanceModerators']);
self::assertArrayKeysMatch(self::AP_USER_DEFAULT_KEYS, $jsonData['instanceAdmins'][0]);
self::assertArrayKeysMatch(self::AP_USER_DEFAULT_KEYS, $jsonData['instanceModerators'][0]);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineBadgesApiTest.php
================================================
getMagazineByName('test');
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/badge", parameters: ['name' => 'test']);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRemoveBadgesFromMagazineAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$badgeManager = $this->badgeManager;
$badge = $badgeManager->create(BadgeDto::create($magazine, 'test'));
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/badge/{$badge->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAddBadgesToMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/badge", parameters: ['name' => 'test'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRemoveBadgesFromMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$badgeManager = $this->badgeManager;
$badge = $badgeManager->create(BadgeDto::create($magazine, 'test'));
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/badge/{$badge->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotAddBadgesMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
$admin = $this->getUserByUsername('admin', isAdmin: true);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:badges');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/badge", parameters: ['name' => 'test'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotRemoveBadgesMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
$admin = $this->getUserByUsername('admin', isAdmin: true);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$badgeManager = $this->badgeManager;
$badge = $badgeManager->create(BadgeDto::create($magazine, 'test'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:badges');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/badge/{$badge->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiOwnerCanAddBadgesMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:badges');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/badge", parameters: ['name' => 'test'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['badges']);
self::assertCount(1, $jsonData['badges']);
self::assertArrayKeysMatch(self::BADGE_RESPONSE_KEYS, $jsonData['badges'][0]);
self::assertEquals('test', $jsonData['badges'][0]['name']);
}
public function testApiOwnerCanRemoveBadgesMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$badgeManager = $this->badgeManager;
$badge = $badgeManager->create(BadgeDto::create($magazine, 'test'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:badges');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/badge/{$badge->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['badges']);
self::assertCount(0, $jsonData['badges']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineCreateApiTest.php
================================================
client->request('POST', '/api/moderate/magazine/new');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', '/api/moderate/magazine/new', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$name = 'test';
$title = 'API Test Magazine';
$description = 'A description';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
'discoverable' => false,
'isPostingRestrictedToMods' => true,
'indexable' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertEquals($name, $jsonData['name']);
self::assertSame($user->getId(), $jsonData['owner']['userId']);
self::assertEquals($description, $jsonData['description']);
self::assertEquals($rules, $jsonData['rules']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['discoverable']);
self::assertTrue($jsonData['isPostingRestrictedToMods']);
self::assertFalse($jsonData['indexable']);
}
public function testApiCannotCreateInvalidMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$title = 'No name';
$description = 'A description';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => null,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'a';
$title = 'Too short name';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'long_name_that_exceeds_the_limit';
$title = 'Too long name';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'invalidch@racters!';
$title = 'Invalid Characters in name';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'nulltitle';
$title = null;
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'shorttitle';
$title = 'as';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'longtitle';
$title = 'Way too long of a title. This can only be 50 characters!';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$name = 'rulesDeprecated';
$title = 'rules are deprecated';
$rules = 'Some rules';
$this->client->jsonRequest(
'POST', '/api/moderate/magazine/new',
parameters: [
'name' => $name,
'title' => $title,
'rules' => $rules,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineDeleteApiTest.php
================================================
getMagazineByName('test');
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiUserCannotDeleteUnownedMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotDeleteUnownedMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
$admin = $this->getUserByUsername('admin', isAdmin: true);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineDeleteIconApiTest.php
================================================
kibbyPath = \dirname(__FILE__, 6).'/assets/kibby_emoji.png';
}
public function testApiCannotDeleteMagazineIconAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/icon");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteMagazineIconWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/icon", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotDeleteMagazineIcon(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
$admin = $this->getUserByUsername('admin', isAdmin: true);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/icon", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanDeleteMagazineIcon(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$upload = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageRepository = $this->imageRepository;
$image = $imageRepository->findOrCreateFromUpload($upload);
self::assertNotNull($image);
$magazine->icon = $image;
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:theme');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(WebTestCase::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['icon']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['icon']);
self::assertSame(96, $jsonData['icon']['width']);
self::assertSame(96, $jsonData['icon']['height']);
self::assertEquals('a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png', $jsonData['icon']['filePath']);
self::assertNull($jsonData['banner']);
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/icon", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineUpdateThemeApiTest::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertNull($jsonData['icon']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineModeratorsApiTest.php
================================================
getMagazineByName('test');
$user = $this->getUserByUsername('notamod');
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRemoveModeratorsFromMagazineAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('yesamod');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $user;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAddModeratorsToMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('notamod');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRemoveModeratorsFromMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('yesamod');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $user;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotAddModeratorsMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$user = $this->getUserByUsername('notamod');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:moderators');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotRemoveModeratorsMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$user = $this->getUserByUsername('yesamod');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$dto = new ModeratorDto($magazine);
$dto->user = $user;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:moderators');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/mod/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiOwnerCanAddModeratorsMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$moderator = $this->getUserByUsername('willbeamod');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:moderators');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/mod/{$moderator->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['moderators']);
self::assertCount(2, $jsonData['moderators']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][1]);
self::assertSame($moderator->getId(), $jsonData['moderators'][1]['userId']);
}
public function testApiOwnerCanRemoveModeratorsMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$moderator = $this->getUserByUsername('yesamod');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $admin;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:moderators');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['moderators']);
self::assertCount(2, $jsonData['moderators']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][1]);
self::assertSame($moderator->getId(), $jsonData['moderators'][1]['userId']);
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/mod/{$moderator->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['moderators']);
self::assertCount(1, $jsonData['moderators']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][0]);
self::assertSame($user->getId(), $jsonData['moderators'][0]['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazinePurgeApiTest.php
================================================
getMagazineByName('test');
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminUserCannotPurgeMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write admin:magazine:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotPurgeMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write admin:magazine:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiOwnerCannotPurgeMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write admin:magazine:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiAdminCanPurgeMagazine(): void
{
$admin = $this->getUserByUsername('JohnDoe', isAdmin: true);
$owner = $this->getUserByUsername('JaneDoe');
$this->client->loginUser($admin);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write admin:magazine:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/magazine/{$magazine->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineRetrieveStatsApiTest.php
================================================
getMagazineByName('test');
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/votes");
self::assertResponseStatusCodeSame(401);
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/content");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineStatsWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/votes", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveMagazineStatsIfNotOwner(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$owner = $this->getUserByUsername('JaneDoe');
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $this->getUserByUsername('JohnDoe');
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:stats');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/votes", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineStats(): void
{
$user = $this->getUserByUsername('JohnDoe');
$user2 = $this->getUserByUsername('JohnDoe2');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$entry = $this->getEntryByTitle('Stats test', body: 'This is gonna be a statistic', magazine: $magazine, user: $user);
$requestStack = $this->requestStack;
$requestStack->push(Request::create('/'));
$dispatcher = $this->eventDispatcher;
$dispatcher->dispatch(new EntryHasBeenSeenEvent($entry));
$favouriteManager = $this->favouriteManager;
$favourite = $favouriteManager->toggle($user, $entry);
$voteManager = $this->voteManager;
$vote = $voteManager->upvote($entry, $user);
$entityManager = $this->entityManager;
$entityManager->persist($favourite);
$entityManager->persist($vote);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:stats');
$token = $codes['token_type'].' '.$codes['access_token'];
// Start a day ago to avoid timezone issues when testing on machines with non-UTC timezones
$startString = rawurlencode($entry->getCreatedAt()->add(\DateInterval::createFromDateString('-1 minute'))->format(\DateTimeImmutable::ATOM));
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/votes?resolution=hour&start=$startString", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::STATS_BY_CONTENT_TYPE_KEYS, $jsonData);
self::assertIsArray($jsonData['entry']);
self::assertCount(1, $jsonData['entry']);
self::assertIsArray($jsonData['entry_comment']);
self::assertEmpty($jsonData['entry_comment']);
self::assertIsArray($jsonData['post']);
self::assertEmpty($jsonData['post']);
self::assertIsArray($jsonData['post_comment']);
self::assertEmpty($jsonData['post_comment']);
self::assertArrayKeysMatch(self::VOTE_ITEM_KEYS, $jsonData['entry'][0]);
self::assertSame(1, $jsonData['entry'][0]['up']);
self::assertSame(0, $jsonData['entry'][0]['down']);
self::assertSame(1, $jsonData['entry'][0]['boost']);
$this->client->request('GET', "/api/stats/magazine/{$magazine->getId()}/content?resolution=hour&start=$startString", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::STATS_BY_CONTENT_TYPE_KEYS, $jsonData);
self::assertIsInt($jsonData['entry']);
self::assertIsInt($jsonData['entry_comment']);
self::assertIsInt($jsonData['post']);
self::assertIsInt($jsonData['post_comment']);
self::assertSame(1, $jsonData['entry']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineTagsApiTest.php
================================================
getMagazineByName('test');
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/tag/test");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRemoveTagsFromMagazineAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$magazine->tags = ['test'];
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/tag/test");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAddTagsToMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRemoveTagsFromMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$magazine->tags = ['test'];
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotAddTagsMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:tags');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiModCannotRemoveTagsMagazine(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$magazine->tags = ['test'];
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:tags');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiOwnerCanAddTagsMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:tags');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['tags']);
self::assertCount(1, $jsonData['tags']);
self::assertEquals('test', $jsonData['tags'][0]);
}
public function testApiOwnerCannotAddWeirdTagsMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:tags');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/tag/test%20Weird", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiOwnerCanRemoveTagsMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$magazine->tags = ['test'];
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:tags');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['tags']);
self::assertCount(1, $jsonData['tags']);
self::assertEquals('test', $jsonData['tags'][0]);
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/tag/test", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertEmpty($jsonData['tags']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateApiTest.php
================================================
getMagazineByName('test');
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateMagazineWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateMagazine(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$magazine->rules = 'Some initial rules';
$this->entityManager->persist($magazine);
$this->entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:update');
$token = $codes['token_type'].' '.$codes['access_token'];
$name = 'test';
$title = 'API Test Magazine';
$description = 'A description';
$rules = 'Some rules';
$this->client->jsonRequest(
'PUT', "/api/moderate/magazine/{$magazine->getId()}",
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'rules' => $rules,
'isAdult' => true,
'discoverable' => false,
'indexable' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(WebTestCase::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertEquals($name, $jsonData['name']);
self::assertSame($user->getId(), $jsonData['owner']['userId']);
self::assertEquals($description, $jsonData['description']);
self::assertEquals($rules, $jsonData['rules']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['discoverable']);
self::assertFalse($jsonData['indexable']);
}
public function testApiCannotUpdateMagazineWithInvalidParams(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:update');
$token = $codes['token_type'].' '.$codes['access_token'];
$name = 'someothername';
$title = 'Different name';
$description = 'A description';
$this->client->jsonRequest(
'PUT', "/api/moderate/magazine/{$magazine->getId()}",
parameters: [
'name' => $name,
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$description = 'short title';
$title = 'as';
$this->client->jsonRequest(
'PUT', "/api/moderate/magazine/{$magazine->getId()}",
parameters: [
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$description = 'long title';
$title = 'Way too long of a title. This can only be 50 characters!';
$this->client->jsonRequest(
'PUT', "/api/moderate/magazine/{$magazine->getId()}",
parameters: [
'title' => $title,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
$rules = 'Some rules';
$description = 'Rules are deprecated';
$this->client->jsonRequest(
'PUT', "/api/moderate/magazine/{$magazine->getId()}",
parameters: [
'rules' => $rules,
'description' => $description,
'isAdult' => false,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(400);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Admin/MagazineUpdateThemeApiTest.php
================================================
kibbyPath = \dirname(__FILE__, 6).'/assets/kibby_emoji.png';
}
public function testApiCannotUpdateMagazineThemeAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/theme");
self::assertResponseStatusCodeSame(401);
}
public function testApiModCannotUpdateMagazineTheme(): void
{
$moderator = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($moderator);
$owner = $this->getUserByUsername('JaneDoe');
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $owner);
$magazineManager = $this->magazineManager;
$dto = new ModeratorDto($magazine);
$dto->user = $moderator;
$dto->addedBy = $owner;
$magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:theme');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/theme", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateMagazineThemeWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/theme", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateMagazineThemeWithCustomCss(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:theme');
$token = $codes['token_type'].' '.$codes['access_token'];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.tmp');
$image = new UploadedFile($tmpPath.'.tmp', 'kibby_emoji.png', 'image/png');
$customCss = 'a {background: red;}';
$this->client->request(
'POST', "/api/moderate/magazine/{$magazine->getId()}/theme",
parameters: [
'customCss' => $customCss,
],
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertStringContainsString($customCss, $jsonData['customCss']);
self::assertIsArray($jsonData['icon']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['icon']);
self::assertNull($jsonData['banner']);
self::assertSame(96, $jsonData['icon']['width']);
self::assertSame(96, $jsonData['icon']['height']);
self::assertEquals('a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png', $jsonData['icon']['filePath']);
}
public function testApiCanUpdateMagazineThemeWithBackgroundImage(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:theme');
$token = $codes['token_type'].' '.$codes['access_token'];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$backgroundImage = 'shape1';
$this->client->request(
'POST', "/api/moderate/magazine/{$magazine->getId()}/theme",
parameters: [
'backgroundImage' => $backgroundImage,
],
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertStringContainsString('/build/images/shape.png', $jsonData['customCss']);
self::assertIsArray($jsonData['icon']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['icon']);
self::assertSame(96, $jsonData['icon']['width']);
self::assertSame(96, $jsonData['icon']['height']);
self::assertEquals($expectedPath, $jsonData['icon']['filePath']);
}
public function testCanUpdateMagazineBanner(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine_admin:theme');
$token = $codes['token_type'].' '.$codes['access_token'];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.tmp');
$image = new UploadedFile($tmpPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'PUT', "/api/moderate/magazine/{$magazine->getId()}/banner",
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(WebTestCase::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertNull($jsonData['icon']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['banner']);
self::assertSame(96, $jsonData['banner']['width']);
self::assertSame(96, $jsonData['banner']['height']);
self::assertEquals('a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png', $jsonData['banner']['filePath']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/MagazineBlockApiTest.php
================================================
getMagazineByName('test');
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/block');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotBlockMagazineWithoutScope(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/block', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanBlockMagazine(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/block', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertNull($jsonData['isUserSubscribed']);
self::assertTrue($jsonData['isBlockedByUser']);
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertNull($jsonData['isUserSubscribed']);
self::assertTrue($jsonData['isBlockedByUser']);
}
public function testApiCannotUnblockMagazineAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unblock');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnblockMagazineWithoutScope(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unblock', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnblockMagazine(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$manager = $this->magazineManager;
$manager->block($magazine, $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unblock', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertNull($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertNull($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/MagazineModlogApiTest.php
================================================
getMagazineByName('test');
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId().'/log');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertEmpty($jsonData['items']);
}
public function testApiCanRetrieveModlogByMagazineIdAnonymouslyWithTypeFilter(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('GET', '/api/magazine/'.$magazine->getId().'/log?types[]='.MagazineLog::CHOICES[0].'&types[]='.MagazineLog::CHOICES[1]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertEmpty($jsonData['items']);
}
public function testApiCanRetrieveMagazineById(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId().'/log', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertEmpty($jsonData['items']);
}
public function testApiCanRetrieveEntryPinnedLog(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$entry = $this->getEntryByTitle('Something to pin', magazine: $magazine);
$this->entryManager->pin($entry, $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/'.$magazine->getId().'/log', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
$item = $jsonData['items'][0];
self::assertArrayKeysMatch(WebTestCase::LOG_ENTRY_KEYS, $item);
self::assertEquals('log_entry_pinned', $item['type']);
self::assertIsArray($item['subject']);
self::assertArrayKeysMatch(WebTestCase::ENTRY_RESPONSE_KEYS, $item['subject']);
self::assertEquals($entry->getId(), $item['subject']['entryId']);
}
public function testApiCanRetrieveUserBannedLog(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$banned = $this->getUserByUsername('troll');
$dto = new MagazineBanDto();
$dto->reason = 'because';
$ban = $this->magazineManager->ban($magazine, $banned, $user, $dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/'.$magazine->getId().'/log', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
$item = $jsonData['items'][0];
self::assertArrayKeysMatch(WebTestCase::LOG_ENTRY_KEYS, $item);
self::assertEquals('log_ban', $item['type']);
self::assertArrayKeysMatch(WebTestCase::BAN_RESPONSE_KEYS, $item['subject']);
self::assertEquals($ban->getId(), $item['subject']['banId']);
}
public function testApiCanRetrieveModeratorAddedLog(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$mod = $this->getUserByUsername('mod');
$dto = new ModeratorDto($magazine, $mod, $user);
$this->magazineManager->addModerator($dto);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/'.$magazine->getId().'/log', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
$item = $jsonData['items'][0];
self::assertArrayKeysMatch(WebTestCase::LOG_ENTRY_KEYS, $item);
self::assertEquals('log_moderator_add', $item['type']);
self::assertArrayKeysMatch(WebTestCase::USER_SMALL_RESPONSE_KEYS, $item['subject']);
self::assertEquals($mod->getId(), $item['subject']['userId']);
self::assertArrayKeysMatch(WebTestCase::USER_SMALL_RESPONSE_KEYS, $item['moderator']);
self::assertEquals($user->getId(), $item['moderator']['userId']);
}
public function testApiModlogReflectsModerationActionsTaken(): void
{
$this->createModlogMessages();
$magazine = $this->getMagazineByName('acme');
$moderator = $magazine->getOwner();
$entityManager = $this->entityManager;
$entityManager->refresh($magazine);
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId().'/log');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
$this->validateModlog($jsonData, $magazine, $moderator);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/MagazineRetrieveApiTest.php
================================================
getMagazineByName('test');
$this->client->request('GET', "/api/magazine/{$magazine->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertSame($magazine->getId(), $jsonData['magazineId']);
self::assertIsArray($jsonData['owner']);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['owner']);
self::assertSame($magazine->getOwner()->getId(), $jsonData['owner']['userId']);
self::assertNull($jsonData['icon']);
self::assertNull($jsonData['banner']);
self::assertEmpty($jsonData['tags']);
self::assertEquals('test', $jsonData['name']);
self::assertIsArray($jsonData['badges']);
self::assertIsArray($jsonData['moderators']);
self::assertCount(1, $jsonData['moderators']);
self::assertIsArray($jsonData['moderators'][0]);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][0]);
self::assertSame($magazine->getOwner()->getId(), $jsonData['moderators'][0]['userId']);
self::assertFalse($jsonData['isAdult']);
// Anonymous access, so these values should be null
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveMagazineById(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertSame($magazine->getId(), $jsonData['magazineId']);
self::assertIsArray($jsonData['owner']);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['owner']);
self::assertSame($magazine->getOwner()->getId(), $jsonData['owner']['userId']);
self::assertNull($jsonData['icon']);
self::assertNull($jsonData['banner']);
self::assertEmpty($jsonData['tags']);
self::assertEquals('test', $jsonData['name']);
self::assertIsArray($jsonData['badges']);
self::assertIsArray($jsonData['moderators']);
self::assertCount(1, $jsonData['moderators']);
self::assertIsArray($jsonData['moderators'][0]);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][0]);
self::assertSame($magazine->getOwner()->getId(), $jsonData['moderators'][0]['userId']);
self::assertFalse($jsonData['isAdult']);
// Scopes for reading subscriptions and blocklists not granted, so these values should be null
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveMagazineByNameAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('GET', '/api/magazine/name/test');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertSame($magazine->getId(), $jsonData['magazineId']);
self::assertIsArray($jsonData['owner']);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['owner']);
self::assertSame($magazine->getOwner()->getId(), $jsonData['owner']['userId']);
self::assertNull($jsonData['icon']);
self::assertNull($jsonData['banner']);
self::assertEmpty($jsonData['tags']);
self::assertEquals('test', $jsonData['name']);
self::assertIsArray($jsonData['badges']);
self::assertIsArray($jsonData['moderators']);
self::assertCount(1, $jsonData['moderators']);
self::assertIsArray($jsonData['moderators'][0]);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][0]);
self::assertSame($magazine->getOwner()->getId(), $jsonData['moderators'][0]['userId']);
self::assertFalse($jsonData['isAdult']);
// Anonymous access, so these values should be null
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveMagazineByName(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/name/test', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
self::assertSame($magazine->getId(), $jsonData['magazineId']);
self::assertIsArray($jsonData['owner']);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['owner']);
self::assertSame($magazine->getOwner()->getId(), $jsonData['owner']['userId']);
self::assertNull($jsonData['icon']);
self::assertNull($jsonData['banner']);
self::assertEmpty($jsonData['tags']);
self::assertEquals('test', $jsonData['name']);
self::assertIsArray($jsonData['badges']);
self::assertIsArray($jsonData['moderators']);
self::assertCount(1, $jsonData['moderators']);
self::assertIsArray($jsonData['moderators'][0]);
self::assertArrayKeysMatch(self::MODERATOR_RESPONSE_KEYS, $jsonData['moderators'][0]);
self::assertSame($magazine->getOwner()->getId(), $jsonData['moderators'][0]['userId']);
self::assertFalse($jsonData['isAdult']);
// Scopes for reading subscriptions and blocklists not granted, so these values should be null
self::assertNull($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiMagazineSubscribeAndBlockFlags(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertFalse($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
}
// The 2 next tests exist because changing the subscription status via MagazineManager after calling the API
// was causing strange doctrine exceptions. If doctrine did not throw exceptions when modifications
// were made, these tests could be rolled into testApiMagazineSubscribeAndBlockFlags above
public function testApiMagazineSubscribeFlagIsTrueWhenSubscribed(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$manager = $this->magazineManager;
$manager->subscribe($magazine, $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertTrue($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
}
public function testApiMagazineBlockFlagIsTrueWhenBlocked(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$manager = $this->magazineManager;
$manager->block($magazine, $user);
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertFalse($jsonData['isUserSubscribed']);
self::assertTrue($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveMagazineCollectionAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('GET', '/api/magazines');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
}
public function testApiCanRetrieveMagazineCollection(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
// Scopes not granted
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCanRetrieveMagazineCollectionMultiplePages(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazines = [];
for ($i = 0; $i < self::MAGAZINE_COUNT; ++$i) {
$magazines[] = $this->getMagazineByNameNoRSAKey("test{$i}");
}
$perPage = max((int) ceil(self::MAGAZINE_COUNT / 2), 1);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazines?perPage={$perPage}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(self::MAGAZINE_COUNT, $jsonData['pagination']['count']);
self::assertSame($perPage, $jsonData['pagination']['perPage']);
self::assertSame(1, $jsonData['pagination']['currentPage']);
self::assertSame(2, $jsonData['pagination']['maxPage']);
self::assertIsArray($jsonData['items']);
self::assertCount($perPage, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertAllValuesFoundByName($magazines, $jsonData['items']);
}
public function testApiCannotRetrieveMagazineSubscriptionsAnonymous(): void
{
$this->client->request('GET', '/api/magazines/subscribed');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineSubscriptionsWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineSubscriptions(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$notSubbedMag = $this->getMagazineByName('someother', $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
// Block scope not granted
self::assertTrue($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCannotRetrieveUserMagazineSubscriptionsAnonymous(): void
{
$user = $this->getUserByUsername('testUser');
$this->client->request('GET', "/api/users/{$user->getId()}/magazines/subscriptions");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveUserMagazineSubscriptionsWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$user = $this->getUserByUsername('testUser');
$this->client->request('GET', "/api/users/{$user->getId()}/magazines/subscriptions", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveUserMagazineSubscriptions(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('testUser');
$user->showProfileSubscriptions = true;
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
$notSubbedMag = $this->getMagazineByName('someother', $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('test', $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/magazines/subscriptions", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
// Block scope not granted
self::assertFalse($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCannotRetrieveUserMagazineSubscriptionsIfSettingTurnedOff(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('testUser');
$user->showProfileSubscriptions = false;
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/magazines/subscriptions", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveModeratedMagazinesAnonymous(): void
{
$this->client->request('GET', '/api/magazines/moderated');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveModeratedMagazinesWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveModeratedMagazines(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$notModdedMag = $this->getMagazineByName('someother', $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:list');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
// Subscribe and block scopes not granted
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertNull($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCannotRetrieveBlockedMagazinesAnonymous(): void
{
$this->client->request('GET', '/api/magazines/blocked');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveBlockedMagazinesWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/blocked', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveBlockedMagazines(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$notBlockedMag = $this->getMagazineByName('someother', $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$manager = $this->magazineManager;
$manager->block($magazine, $this->getUserByUsername('JohnDoe'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazines/blocked', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazineId']);
// Subscribe and block scopes not granted
self::assertNull($jsonData['items'][0]['isUserSubscribed']);
self::assertTrue($jsonData['items'][0]['isBlockedByUser']);
}
public function testApiCanRetrieveAbandonedMagazine(): void
{
$abandoningUser = $this->getUserByUsername('JohnDoe');
$activeUser = $this->getUserByUsername('DoeJohn');
$magazine1 = $this->getMagazineByName('test1', $abandoningUser);
$magazine2 = $this->getMagazineByName('test2', $abandoningUser);
$magazine3 = $this->getMagazineByName('test3', $activeUser);
$abandoningUser->lastActive = new \DateTime('-6 months');
$activeUser->lastActive = new \DateTime('-2 days');
$this->userRepository->save($abandoningUser, true);
$this->userRepository->save($activeUser, true);
$this->client->request('GET', '/api/magazines?abandoned=true&federation=local');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine1->getId(), $jsonData['items'][0]['magazineId']);
self::assertSame($magazine2->getId(), $jsonData['items'][1]['magazineId']);
}
public function testApiCanRetrieveAbandonedMagazineSortedByOwner(): void
{
$abandoningUser1 = $this->getUserByUsername('user1');
$abandoningUser2 = $this->getUserByUsername('user2');
$abandoningUser3 = $this->getUserByUsername('user3');
$magazine1 = $this->getMagazineByName('test1', $abandoningUser1);
$magazine2 = $this->getMagazineByName('test2', $abandoningUser2);
$magazine3 = $this->getMagazineByName('test3', $abandoningUser3);
$abandoningUser1->lastActive = new \DateTime('-6 months');
$abandoningUser2->lastActive = new \DateTime('-5 months');
$abandoningUser3->lastActive = new \DateTime('-7 months');
$this->userRepository->save($abandoningUser1, true);
$this->userRepository->save($abandoningUser2, true);
$this->userRepository->save($abandoningUser3, true);
$this->client->request('GET', '/api/magazines?abandoned=true&federation=local&sort=ownerLastActive');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($magazine1->getId(), $jsonData['items'][1]['magazineId']);
self::assertSame($magazine2->getId(), $jsonData['items'][2]['magazineId']);
self::assertSame($magazine3->getId(), $jsonData['items'][0]['magazineId']);
}
public static function assertAllValuesFoundByName(array $magazines, array $values, string $message = '')
{
$nameMap = array_column($magazines, null, 'name');
$containsMagazine = fn (bool $result, array $item) => $result && null !== $nameMap[$item['name']];
self::assertTrue(array_reduce($values, $containsMagazine, true), $message);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/MagazineRetrieveThemeApiTest.php
================================================
getMagazineByName('test');
$magazine->customCss = '.test {}';
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId().'/theme');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertEquals('.test {}', $jsonData['customCss']);
}
public function testApiCanRetrieveMagazineThemeById(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$magazine->customCss = '.test {}';
$entityManager = $this->entityManager;
$entityManager->persist($magazine);
$entityManager->flush();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId().'/theme', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MAGAZINE_THEME_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertEquals('.test {}', $jsonData['customCss']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/MagazineSubscribeApiTest.php
================================================
getMagazineByName('test');
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/subscribe');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSubscribeToMagazineWithoutScope(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/subscribe', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSubscribeToMagazine(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/subscribe', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertTrue($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertTrue($jsonData['isUserSubscribed']);
self::assertFalse($jsonData['isBlockedByUser']);
}
public function testApiCannotUnsubscribeFromMagazineAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unsubscribe');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnsubscribeFromMagazineWithoutScope(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:block');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unsubscribe', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnsubscribeFromMagazine(): void
{
$user = $this->getUserByUsername('testuser');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$manager = $this->magazineManager;
$manager->subscribe($magazine, $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/magazine/'.(string) $magazine->getId().'/unsubscribe', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertFalse($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
$this->client->request('GET', '/api/magazine/'.(string) $magazine->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_RESPONSE_KEYS, $jsonData);
// Scopes for reading subscriptions and blocklists granted, so these values should be filled
self::assertFalse($jsonData['isUserSubscribed']);
self::assertNull($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineActionReportsApiTest.php
================================================
getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/accept");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRejectReportAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/reject");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAcceptReportWithoutScope(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/accept", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRejectReportWithoutScope(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/reject", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotAcceptReportIfNotMod(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:action');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/accept", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRejectReportIfNotMod(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:action');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/reject", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanAcceptReport(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:action');
$token = $codes['token_type'].' '.$codes['access_token'];
$consideredAt = new \DateTimeImmutable();
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/accept", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveReportsApiTest::REPORT_RESPONSE_KEYS, $jsonData);
self::assertEquals('entry_report', $jsonData['type']);
self::assertEquals($report->reason, $jsonData['reason']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reported']);
self::assertSame($reportedUser->getId(), $jsonData['reported']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reporting']);
self::assertSame($user->getId(), $jsonData['reporting']['userId']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['subject']);
self::assertEquals($entry->getId(), $jsonData['subject']['entryId']);
self::assertEquals('trashed', $jsonData['subject']['visibility']);
self::assertEquals($entry->body, $jsonData['subject']['body']);
self::assertEquals('approved', $jsonData['status']);
self::assertSame(1, $jsonData['weight']);
self::assertEqualsWithDelta($report->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp(), 10.0);
self::assertEqualsWithDelta($consideredAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['consideredAt'])->getTimestamp(), 10.0);
self::assertNotNull($jsonData['consideredBy']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['consideredBy']);
self::assertSame($user->getId(), $jsonData['consideredBy']['userId']);
}
public function testApiCanRejectReport(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('testuser');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:action');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}/reject", server: ['HTTP_AUTHORIZATION' => $token]);
$consideredAt = new \DateTimeImmutable();
$adjustedConsideredAt = floor($consideredAt->getTimestamp() / 1000);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
$adjustedReceivedConsideredAt = floor(\DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp() / 1000);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveReportsApiTest::REPORT_RESPONSE_KEYS, $jsonData);
self::assertEquals('entry_report', $jsonData['type']);
self::assertEquals($report->reason, $jsonData['reason']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reported']);
self::assertSame($reportedUser->getId(), $jsonData['reported']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reporting']);
self::assertSame($user->getId(), $jsonData['reporting']['userId']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['subject']);
self::assertEquals($entry->getId(), $jsonData['subject']['entryId']);
self::assertEquals('visible', $jsonData['subject']['visibility']);
self::assertEquals($entry->body, $jsonData['subject']['body']);
self::assertEquals('rejected', $jsonData['status']);
self::assertSame(1, $jsonData['weight']);
self::assertEqualsWithDelta($report->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp(), 10.0);
self::assertEqualsWithDelta($adjustedConsideredAt, $adjustedReceivedConsideredAt, 10.0);
self::assertNotNull($jsonData['consideredBy']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['consideredBy']);
self::assertSame($user->getId(), $jsonData['consideredBy']['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineBanApiTest.php
================================================
getMagazineByName('test');
$user = $this->getUserByUsername('testuser');
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateMagazineBanWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('testuser');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotCreateMagazineBanIfNotMod(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$user = $this->getUserByUsername('testuser');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('POST', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateMagazineBan(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$bannedUser = $this->getUserByUsername('hapless_fool');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$reason = 'you got banned through the API, how does that make you feel?';
$expiredAt = (new \DateTimeImmutable('+1 hour'))->format(\DateTimeImmutable::ATOM);
$this->client->jsonRequest(
'POST', "/api/moderate/magazine/{$magazine->getId()}/ban/{$bannedUser->getId()}",
parameters: [
'reason' => $reason,
'expiredAt' => $expiredAt,
],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveBansApiTest::BAN_RESPONSE_KEYS, $jsonData);
self::assertEquals($reason, $jsonData['reason']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['bannedUser']);
self::assertSame($bannedUser->getId(), $jsonData['bannedUser']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['bannedBy']);
self::assertSame($user->getId(), $jsonData['bannedBy']['userId']);
self::assertEquals($expiredAt, $jsonData['expiredAt']);
self::assertFalse($jsonData['expired']);
}
public function testApiCannotDeleteMagazineBanAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('testuser');
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteMagazineBanWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('testuser');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteMagazineBanIfNotMod(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$user = $this->getUserByUsername('testuser');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/ban/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteMagazineBan(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$bannedUser = $this->getUserByUsername('hapless_fool');
$magazineManager = $this->magazineManager;
$ban = MagazineBanDto::create('test ban <3');
$magazineManager->ban($magazine, $bannedUser, $user, $ban);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$expiredAt = new \DateTimeImmutable();
$this->client->request('DELETE', "/api/moderate/magazine/{$magazine->getId()}/ban/{$bannedUser->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MagazineRetrieveBansApiTest::BAN_RESPONSE_KEYS, $jsonData);
self::assertEquals($ban->reason, $jsonData['reason']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['bannedUser']);
self::assertSame($bannedUser->getId(), $jsonData['bannedUser']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['bannedBy']);
self::assertSame($user->getId(), $jsonData['bannedBy']['userId']);
$actualExpiry = \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['expiredAt']);
// Hopefully the API responds fast enough that there is only a max delta of 10 second between these two timestamps
self::assertEqualsWithDelta($expiredAt->getTimestamp(), $actualExpiry->getTimestamp(), 10.0);
self::assertTrue($jsonData['expired']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineModOwnerRequestApiTest.php
================================================
getMagazineByName('test');
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/toggle");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotToggleModRequestWithoutScope(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanToggleModRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(['created'], $jsonData);
self::assertTrue($jsonData['created']);
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(['created'], $jsonData);
self::assertFalse($jsonData['created']);
}
public function testApiCannotAcceptModRequestAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/accept/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAcceptModRequestWithoutScope(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/accept/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanAcceptModRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoeTheSecond');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/accept/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
self::assertSame('', $this->client->getResponse()->getContent());
$modRequest = $this->entityManager->getRepository(ModeratorRequest::class)->findOneBy([
'magazine' => $magazine,
'user' => $user,
]);
self::assertNull($modRequest);
$magazine = $this->magazineRepository->findOneBy(['id' => $magazine->getId()]);
$user = $this->userRepository->findOneBy(['id' => $user->getId()]);
self::assertTrue($magazine->userIsModerator($user));
}
public function testApiCanRejectModRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoeTheSecond');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/modRequest/reject/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
self::assertSame('', $this->client->getResponse()->getContent());
$modRequest = $this->entityManager->getRepository(ModeratorRequest::class)->findOneBy([
'magazine' => $magazine,
'user' => $user,
]);
self::assertNull($modRequest);
$magazine = $this->magazineRepository->findOneBy(['id' => $magazine->getId()]);
$user = $this->userRepository->findOneBy(['id' => $user->getId()]);
self::assertFalse($magazine->userIsModerator($user));
}
public function testApiCannotListModRequestsAnonymously(): void
{
$this->client->request('GET', '/api/moderate/modRequest/list');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotListModRequestsWithoutScope(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/modRequest/list', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotListModRequestsForInvalidMagazineId(): void
{
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/modRequest/list?magazine=a', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCannotListModRequestsForMissingMagazine(): void
{
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/modRequest/list?magazine=99', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCanListModRequestsForMagazine(): void
{
$magazine1 = $this->getMagazineByName('Magazine 1');
$magazine2 = $this->getMagazineByName('Magazine 2');
$magazine3 = $this->getMagazineByName('Magazine 3');
$user1 = $this->getUserByUsername('User 1');
$user2 = $this->getUserByUsername('User 2');
$this->magazineManager->toggleModeratorRequest($magazine1, $user1);
$this->magazineManager->toggleModeratorRequest($magazine1, $user2);
$this->magazineManager->toggleModeratorRequest($magazine2, $user2);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/modRequest/list?magazine={$magazine1->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertCount(2, $jsonData);
self::assertTrue(array_all($jsonData, function ($item) use ($magazine1, $user1, $user2) {
return $item['magazine']['magazineId'] === $magazine1->getId()
&& ($item['user']['userId'] === $user1->getId() || $item['user']['userId'] === $user2->getId());
}));
self::assertNotSame($jsonData[0]['user']['userId'], $jsonData[1]['user']['userId']);
}
public function testApiCanListModRequestsForAllMagazines(): void
{
$magazine1 = $this->getMagazineByName('Magazine 1');
$magazine2 = $this->getMagazineByName('Magazine 2');
$magazine3 = $this->getMagazineByName('Magazine 3');
$user1 = $this->getUserByUsername('User 1');
$user2 = $this->getUserByUsername('User 2');
$this->magazineManager->toggleModeratorRequest($magazine1, $user1);
$this->magazineManager->toggleModeratorRequest($magazine1, $user2);
$this->magazineManager->toggleModeratorRequest($magazine2, $user2);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/modRequest/list', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertCount(3, $jsonData);
self::assertTrue(array_all($jsonData, function ($item) use ($magazine1, $magazine2, $user1, $user2) {
return ($item['magazine']['magazineId'] === $magazine1->getId() && $item['user']['userId'] === $user1->getId())
|| ($item['magazine']['magazineId'] === $magazine1->getId() && $item['user']['userId'] === $user2->getId())
|| ($item['magazine']['magazineId'] === $magazine2->getId() && $item['user']['userId'] === $user2->getId());
}));
}
public function testApiCannotToggleOwnerRequestAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/toggle");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotToggleOwnerRequestWithoutScope(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanToggleOwnerRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'magazine:subscribe');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(['created'], $jsonData);
self::assertTrue($jsonData['created']);
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/toggle", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(['created'], $jsonData);
self::assertFalse($jsonData['created']);
}
public function testApiCannotAcceptOwnerRequestAnonymously(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/accept/{$user->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotAcceptOwnerRequestWithoutScope(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoe');
$this->magazineManager->toggleModeratorRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/accept/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanAcceptOwnerRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoeTheSecond');
$this->magazineManager->toggleOwnershipRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/accept/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
self::assertSame('', $this->client->getResponse()->getContent());
$ownerRequest = $this->entityManager->getRepository(MagazineOwnershipRequest::class)->findOneBy([
'magazine' => $magazine,
'user' => $user,
]);
self::assertNull($ownerRequest);
$magazine = $this->magazineRepository->findOneBy(['id' => $magazine->getId()]);
$user = $this->userRepository->findOneBy(['id' => $user->getId()]);
self::assertTrue($magazine->userIsOwner($user));
}
public function testApiCanRejectOwnerRequest(): void
{
$magazine = $this->getMagazineByName('test');
$user = $this->getUserByUsername('JohnDoeTheSecond');
$this->magazineManager->toggleOwnershipRequest($magazine, $user);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/moderate/magazine/{$magazine->getId()}/ownerRequest/reject/{$user->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
self::assertSame('', $this->client->getResponse()->getContent());
$ownerRequest = $this->entityManager->getRepository(ModeratorRequest::class)->findOneBy([
'magazine' => $magazine,
'user' => $user,
]);
self::assertNull($ownerRequest);
$magazine = $this->magazineRepository->findOneBy(['id' => $magazine->getId()]);
$user = $this->userRepository->findOneBy(['id' => $user->getId()]);
self::assertFalse($magazine->userIsOwner($user));
self::assertFalse($magazine->userIsModerator($user));
}
public function testApiCannotListOwnerRequestsAnonymously(): void
{
$this->client->request('GET', '/api/moderate/ownerRequest/list');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotListOwnerRequestsWithoutScope(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/ownerRequest/list', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotListOwnerRequestsForInvalidMagazineId(): void
{
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/ownerRequest/list?magazine=a', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCannotListOwnerRequestsForMissingMagazine(): void
{
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/ownerRequest/list?magazine=99', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCanListOwnerRequestsForMagazine(): void
{
$magazine1 = $this->getMagazineByName('Magazine 1');
$magazine2 = $this->getMagazineByName('Magazine 2');
$magazine3 = $this->getMagazineByName('Magazine 3');
$user1 = $this->getUserByUsername('User 1');
$user2 = $this->getUserByUsername('User 2');
$this->magazineManager->toggleOwnershipRequest($magazine1, $user1);
$this->magazineManager->toggleOwnershipRequest($magazine1, $user2);
$this->magazineManager->toggleOwnershipRequest($magazine2, $user2);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/ownerRequest/list?magazine={$magazine1->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertCount(2, $jsonData);
self::assertTrue(array_all($jsonData, function ($item) use ($magazine1, $user1, $user2) {
return $item['magazine']['magazineId'] === $magazine1->getId()
&& ($item['user']['userId'] === $user1->getId() || $item['user']['userId'] === $user2->getId());
}));
self::assertNotSame($jsonData[0]['user']['userId'], $jsonData[1]['user']['userId']);
}
public function testApiCanListOwnerRequestsForAllMagazines(): void
{
$magazine1 = $this->getMagazineByName('Magazine 1');
$magazine2 = $this->getMagazineByName('Magazine 2');
$magazine3 = $this->getMagazineByName('Magazine 3');
$user1 = $this->getUserByUsername('User 1');
$user2 = $this->getUserByUsername('User 2');
$this->magazineManager->toggleOwnershipRequest($magazine1, $user1);
$this->magazineManager->toggleOwnershipRequest($magazine1, $user2);
$this->magazineManager->toggleOwnershipRequest($magazine2, $user2);
$adminUser = $this->getUserByUsername('Admin');
$this->setAdmin($adminUser);
$this->client->loginUser($adminUser);
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:magazine:moderate');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/moderate/ownerRequest/list', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertCount(3, $jsonData);
self::assertTrue(array_all($jsonData, function ($item) use ($magazine1, $magazine2, $user1, $user2) {
return ($item['magazine']['magazineId'] === $magazine1->getId() && $item['user']['userId'] === $user1->getId())
|| ($item['magazine']['magazineId'] === $magazine1->getId() && $item['user']['userId'] === $user2->getId())
|| ($item['magazine']['magazineId'] === $magazine2->getId() && $item['user']['userId'] === $user2->getId());
}));
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineRetrieveBansApiTest.php
================================================
getMagazineByName('test');
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/bans");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineBansWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/bans", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveMagazineBansIfNotMod(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/bans", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineBans(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$bannedUser = $this->getUserByUsername('hapless_fool');
$magazineManager = $this->magazineManager;
$ban = MagazineBanDto::create('test ban :)');
$magazineManager->ban($magazine, $bannedUser, $user, $ban);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:ban:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/bans", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::BAN_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals($ban->reason, $jsonData['items'][0]['reason']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['bannedUser']);
self::assertSame($bannedUser->getId(), $jsonData['items'][0]['bannedUser']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['bannedBy']);
self::assertSame($user->getId(), $jsonData['items'][0]['bannedBy']['userId']);
self::assertNull($jsonData['items'][0]['expiredAt']);
self::assertFalse($jsonData['items'][0]['expired']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineRetrieveReportsApiTest.php
================================================
getUserByUsername('JohnDoe');
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineReportByIdWithoutScope(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveMagazineReportByIdIfNotMod(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineReportById(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports/{$report->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::REPORT_RESPONSE_KEYS, $jsonData);
self::assertEquals($report->reason, $jsonData['reason']);
self::assertEquals('entry_report', $jsonData['type']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reported']);
self::assertSame($reportedUser->getId(), $jsonData['reported']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['reporting']);
self::assertSame($user->getId(), $jsonData['reporting']['userId']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['subject']);
self::assertSame($entry->getId(), $jsonData['subject']['entryId']);
self::assertEquals('pending', $jsonData['status']);
self::assertSame(1, $jsonData['weight']);
self::assertNull($jsonData['consideredAt']);
self::assertNull($jsonData['consideredBy']);
self::assertEqualsWithDelta($report->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp(), 10.0);
}
public function testApiCannotRetrieveMagazineReportsAnonymous(): void
{
$magazine = $this->getMagazineByName('test');
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineReportsWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveMagazineReportsIfNotMod(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineReports(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Report test', body: 'This is gonna be reported', magazine: $magazine, user: $reportedUser);
$reportManager = $this->reportManager;
$report = $reportManager->report(ReportDto::create($entry, 'I don\'t like it'), $user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:reports:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/reports", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::REPORT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals($report->reason, $jsonData['items'][0]['reason']);
self::assertEquals('entry_report', $jsonData['items'][0]['type']);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['reported']);
self::assertSame($reportedUser->getId(), $jsonData['items'][0]['reported']['userId']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['reporting']);
self::assertSame($user->getId(), $jsonData['items'][0]['reporting']['userId']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertSame($entry->getId(), $jsonData['items'][0]['subject']['entryId']);
self::assertEquals('pending', $jsonData['items'][0]['status']);
self::assertSame(1, $jsonData['items'][0]['weight']);
self::assertNull($jsonData['items'][0]['consideredAt']);
self::assertNull($jsonData['items'][0]['consideredBy']);
self::assertEqualsWithDelta($report->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['items'][0]['createdAt'])->getTimestamp(), 10.0);
}
}
================================================
FILE: tests/Functional/Controller/Api/Magazine/Moderate/MagazineRetrieveTrashApiTest.php
================================================
getMagazineByName('test');
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/trash");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRetrieveMagazineTrashWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$codes = self::getAuthorizationCodeTokenResponse($this->client);
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveMagazineTrashIfNotMod(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:trash:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$magazine = $this->getMagazineByName('test', $this->getUserByUsername('JaneDoe'));
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveMagazineTrash(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
self::createOAuth2AuthCodeClient();
$magazine = $this->getMagazineByName('test');
$reportedUser = $this->getUserByUsername('hapless_fool');
$entry = $this->getEntryByTitle('Delete test', body: 'This is gonna be deleted', magazine: $magazine, user: $reportedUser);
$entryManager = $this->entryManager;
$entryManager->delete($user, $entry);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read write moderate:magazine:trash:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/moderate/magazine/{$magazine->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
$trashedEntryResponseKeys = array_merge(self::ENTRY_RESPONSE_KEYS, ['itemType']);
self::assertArrayKeysMatch($trashedEntryResponseKeys, $jsonData['items'][0]);
self::assertArrayKeysMatch(MagazineRetrieveApiTest::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame($entry->getId(), $jsonData['items'][0]['entryId']);
self::assertEquals($entry->body, $jsonData['items'][0]['body']);
self::assertEquals(VisibilityInterface::VISIBILITY_TRASHED, $jsonData['items'][0]['visibility']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Message/MessageReadApiTest.php
================================================
createMessage($this->getUserByUsername('JohnDoe'), $this->getUserByUsername('JaneDoe'), 'test message');
$this->client->request('PUT', "/api/messages/{$message->getId()}/read");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotMarkMessagesReadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$message = $this->createMessage($this->getUserByUsername('JohnDoe'), $this->getUserByUsername('JaneDoe'), 'test message');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotMarkOtherUsersMessagesRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$message = $this->createMessage($messagedUser, $messagingUser, 'test message');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanMarkMessagesRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$thread = $this->createMessageThread($user, $messagingUser, 'test message');
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData);
self::assertSame($message->getId(), $jsonData['messageId']);
self::assertSame($thread->getId(), $jsonData['threadId']);
self::assertEquals('test message', $jsonData['body']);
self::assertEquals(Message::STATUS_READ, $jsonData['status']);
self::assertSame($message->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp());
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['sender']);
self::assertSame($messagingUser->getId(), $jsonData['sender']['userId']);
}
public function testApiCannotMarkMessagesUnreadAnonymous(): void
{
$message = $this->createMessage($this->getUserByUsername('JohnDoe'), $this->getUserByUsername('JaneDoe'), 'test message');
$this->client->request('PUT', "/api/messages/{$message->getId()}/unread");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotMarkMessagesUnreadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$message = $this->createMessage($this->getUserByUsername('JohnDoe'), $this->getUserByUsername('JaneDoe'), 'test message');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotMarkOtherUsersMessagesUnread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$message = $this->createMessage($messagedUser, $messagingUser, 'test message');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanMarkMessagesUnread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$thread = $this->createMessageThread($user, $messagingUser, 'test message');
/** @var Message $message */
$message = $thread->messages->get(0);
$messageManager = $this->messageManager;
$messageManager->readMessage($message, $user, flush: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/messages/{$message->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData);
self::assertSame($message->getId(), $jsonData['messageId']);
self::assertSame($thread->getId(), $jsonData['threadId']);
self::assertEquals('test message', $jsonData['body']);
self::assertEquals(Message::STATUS_NEW, $jsonData['status']);
self::assertSame($message->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp());
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['sender']);
self::assertSame($messagingUser->getId(), $jsonData['sender']['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Message/MessageRetrieveApiTest.php
================================================
client->request('GET', '/api/messages');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetMessagesWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/messages', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetMessages(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/messages', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::MESSAGE_THREAD_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($thread->getId(), $jsonData['items'][0]['threadId']);
self::assertSame(1, $jsonData['items'][0]['messageCount']);
self::assertIsArray($jsonData['items'][0]['messages']);
self::assertCount(1, $jsonData['items'][0]['messages']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['items'][0]['messages'][0]);
self::assertSame($message->getId(), $jsonData['items'][0]['messages'][0]['messageId']);
self::assertSame($thread->getId(), $jsonData['items'][0]['messages'][0]['threadId']);
self::assertEquals('test message', $jsonData['items'][0]['messages'][0]['body']);
self::assertEquals('new', $jsonData['items'][0]['messages'][0]['status']);
self::assertSame($message->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['items'][0]['messages'][0]['createdAt'])->getTimestamp());
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['messages'][0]['sender']);
self::assertSame($messagingUser->getId(), $jsonData['items'][0]['messages'][0]['sender']['userId']);
}
public function testApiCannotGetMessageByIdAnonymous(): void
{
$this->client->request('GET', '/api/messages/1');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetMessageByIdWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/messages/1', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotGetOtherUsersMessageById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $messagedUser);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/messages/{$message->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetMessageById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/messages/{$message->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData);
self::assertSame($message->getId(), $jsonData['messageId']);
self::assertSame($thread->getId(), $jsonData['threadId']);
self::assertEquals('test message', $jsonData['body']);
self::assertEquals('new', $jsonData['status']);
self::assertSame($message->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['createdAt'])->getTimestamp());
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['sender']);
self::assertSame($messagingUser->getId(), $jsonData['sender']['userId']);
}
public function testApiCannotGetMessageThreadByIdAnonymous(): void
{
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $messagedUser);
$this->client->request('GET', "/api/messages/thread/{$thread->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetMessageThreadByIdWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$this->client->loginUser($user);
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $messagedUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/messages/thread/{$thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotGetOtherUsersMessageThreadById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $messagedUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/messages/thread/{$thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetMessageThreadById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/messages/thread/{$thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(array_merge(self::PAGINATED_KEYS, ['participants']), $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['participants']);
self::assertCount(2, $jsonData['participants']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame($message->getId(), $jsonData['items'][0]['messageId']);
self::assertSame($thread->getId(), $jsonData['items'][0]['threadId']);
self::assertEquals('test message', $jsonData['items'][0]['body']);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertSame($message->createdAt->getTimestamp(), \DateTimeImmutable::createFromFormat(\DateTimeImmutable::ATOM, $jsonData['items'][0]['createdAt'])->getTimestamp());
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['sender']);
self::assertSame($messagingUser->getId(), $jsonData['items'][0]['sender']['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Message/MessageThreadCreateApiTest.php
================================================
getUserByUsername('JohnDoe');
$this->client->jsonRequest('POST', "/api/users/{$messagedUser->getId()}/message", parameters: ['body' => 'test message']);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateThreadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$messagedUser = $this->getUserByUsername('JaneDoe');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/users/{$messagedUser->getId()}/message", parameters: ['body' => 'test message'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateThread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagedUser = $this->getUserByUsername('JaneDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/users/{$messagedUser->getId()}/message", parameters: ['body' => 'test message'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MessageRetrieveApiTest::MESSAGE_THREAD_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['participants']);
self::assertCount(2, $jsonData['participants']);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['participants'][0]);
self::assertTrue($user->getId() === $jsonData['participants'][0]['userId'] || $messagedUser->getId() === $jsonData['participants'][0]['userId']);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['participants'][1]);
self::assertTrue($user->getId() === $jsonData['participants'][1]['userId'] || $messagedUser->getId() === $jsonData['participants'][1]['userId']);
self::assertSame(1, $jsonData['messageCount']);
self::assertNotNull($jsonData['threadId']);
self::assertIsArray($jsonData['messages']);
self::assertCount(1, $jsonData['messages']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['messages'][0]);
self::assertEquals('test message', $jsonData['messages'][0]['body']);
self::assertEquals(Message::STATUS_NEW, $jsonData['messages'][0]['status']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['messages'][0]['sender']);
self::assertSame($user->getId(), $jsonData['messages'][0]['sender']['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Message/MessageThreadReplyApiTest.php
================================================
getUserByUsername('JohnDoe');
$from = $this->getUserByUsername('JaneDoe');
$thread = $this->createMessageThread($to, $from, 'starting a thread');
$this->client->jsonRequest('POST', "/api/messages/thread/{$thread->getId()}/reply", parameters: ['body' => 'test message']);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotReplyToThreadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$from = $this->getUserByUsername('JaneDoe');
$thread = $this->createMessageThread($user, $from, 'starting a thread');
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/messages/thread/{$thread->getId()}/reply", parameters: ['body' => 'test message'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanReplyToThread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$from = $this->getUserByUsername('JaneDoe');
$thread = $this->createMessageThread($user, $from, 'starting a thread');
// Fake when the message was created at so that the newest to oldest order can be reliably determined
$thread->messages->get(0)->createdAt = new \DateTimeImmutable('-5 seconds');
$entityManager = $this->entityManager;
$entityManager->persist($thread);
$entityManager->flush();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:message:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/messages/thread/{$thread->getId()}/reply", parameters: ['body' => 'test message'], server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(MessageRetrieveApiTest::MESSAGE_THREAD_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['participants']);
self::assertCount(2, $jsonData['participants']);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['participants'][0]);
self::assertTrue($user->getId() === $jsonData['participants'][0]['userId'] || $from->getId() === $jsonData['participants'][0]['userId']);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['participants'][1]);
self::assertTrue($user->getId() === $jsonData['participants'][1]['userId'] || $from->getId() === $jsonData['participants'][1]['userId']);
self::assertSame(2, $jsonData['messageCount']);
self::assertNotNull($jsonData['threadId']);
self::assertIsArray($jsonData['messages']);
self::assertCount(2, $jsonData['messages']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['messages'][0]);
// Newest first
self::assertEquals('test message', $jsonData['messages'][0]['body']);
self::assertEquals(Message::STATUS_NEW, $jsonData['messages'][0]['status']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['messages'][0]['sender']);
self::assertSame($user->getId(), $jsonData['messages'][0]['sender']['userId']);
self::assertEquals('starting a thread', $jsonData['messages'][1]['body']);
self::assertEquals(Message::STATUS_NEW, $jsonData['messages'][1]['status']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['messages'][1]['sender']);
self::assertSame($from->getId(), $jsonData['messages'][1]['sender']['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Notification/AdminNotificationRetrieveApiTest.php
================================================
getUserByUsername('JohnDoe', isAdmin: true);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$createdAt = new \DateTimeImmutable();
$createDto = UserDto::create('new_here', email: 'user@example.com', createdAt: $createdAt, applicationText: 'hello there');
$createDto->plainPassword = '1234';
$this->userManager->create($createDto, false, false, false);
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertCount(1, $jsonData['items']);
$item = $jsonData['items'][0];
self::assertArrayKeysMatch(NotificationRetrieveApiTest::NOTIFICATION_RESPONSE_KEYS, $item);
self::assertEquals('new_signup', $item['type']);
self::assertEquals('new', $item['status']);
self::assertNull($item['reportId']);
$subject = $item['subject'];
self::assertIsArray($subject);
self::assertArrayKeysMatch(self::USER_SIGNUP_RESPONSE_KEYS, $subject);
self::assertNotEquals(0, $subject['userId']);
self::assertEquals('new_here', $subject['username']);
self::assertEquals('user@example.com', $subject['email']);
self::assertEquals($createdAt->format(\DateTimeInterface::ATOM), $subject['createdAt']);
self::assertEquals('hello there', $subject['applicationText']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Notification/NotificationDeleteApiTest.php
================================================
createMessageNotification();
$this->client->request('DELETE', "/api/notifications/{$notification->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteNotificationByIdWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/notifications/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersNotificationById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$notification = $this->createMessageNotification($messagedUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/notifications/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteNotificationById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/notifications/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$notificationRepository = $this->notificationRepository;
$notification = $notificationRepository->find($notification->getId());
self::assertNull($notification);
}
public function testApiCannotDeleteAllNotificationsAnonymous(): void
{
$this->createMessageNotification();
$this->client->request('DELETE', '/api/notifications');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteAllNotificationsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$this->createMessageNotification();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', '/api/notifications', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteAllNotifications(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', '/api/notifications', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$notificationRepository = $this->notificationRepository;
$notification = $notificationRepository->find($notification->getId());
self::assertNull($notification);
}
}
================================================
FILE: tests/Functional/Controller/Api/Notification/NotificationReadApiTest.php
================================================
createMessageNotification();
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/read");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotMarkNotificationReadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotMarkOtherUsersNotificationRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$notification = $this->createMessageNotification($messagedUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanMarkNotificationRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/read", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(NotificationRetrieveApiTest::NOTIFICATION_RESPONSE_KEYS, $jsonData);
self::assertEquals('read', $jsonData['status']);
self::assertEquals('message_notification', $jsonData['type']);
self::assertIsArray($jsonData['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['subject']);
self::assertNull($jsonData['subject']['messageId']);
self::assertNull($jsonData['subject']['threadId']);
self::assertNull($jsonData['subject']['sender']);
self::assertNull($jsonData['subject']['status']);
self::assertNull($jsonData['subject']['createdAt']);
self::assertEquals('This app has not received permission to read your messages.', $jsonData['subject']['body']);
}
public function testApiCannotMarkNotificationUnreadAnonymous(): void
{
$notification = $this->createMessageNotification();
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/unread");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotMarkNotificationUnreadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotMarkOtherUsersNotificationUnread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$notification = $this->createMessageNotification($messagedUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanMarkNotificationUnread(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$notification->status = Notification::STATUS_READ;
$entityManager = $this->entityManager;
$entityManager->persist($notification);
$entityManager->flush();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/notifications/{$notification->getId()}/unread", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(NotificationRetrieveApiTest::NOTIFICATION_RESPONSE_KEYS, $jsonData);
self::assertEquals('new', $jsonData['status']);
self::assertEquals('message_notification', $jsonData['type']);
self::assertIsArray($jsonData['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['subject']);
self::assertNull($jsonData['subject']['messageId']);
self::assertNull($jsonData['subject']['threadId']);
self::assertNull($jsonData['subject']['sender']);
self::assertNull($jsonData['subject']['status']);
self::assertNull($jsonData['subject']['createdAt']);
self::assertEquals('This app has not received permission to read your messages.', $jsonData['subject']['body']);
}
public function testApiCannotMarkAllNotificationsReadAnonymous(): void
{
$this->createMessageNotification();
$this->client->request('PUT', '/api/notifications/read');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotMarkAllNotificationsReadWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$this->createMessageNotification();
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/notifications/read', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanMarkAllNotificationsRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', '/api/notifications/read', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$notificationRepository = $this->notificationRepository;
$notification = $notificationRepository->find($notification->getId());
self::assertNotNull($notification);
self::assertEquals('read', $notification->status);
}
}
================================================
FILE: tests/Functional/Controller/Api/Notification/NotificationRetrieveApiTest.php
================================================
client->request('GET', '/api/notifications/all');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetNotificationsByStatusWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetNotificationsByStatusMessagesRedactedWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$notificationManager = $this->notificationManager;
$notificationManager->readMessageNotification($message, $user);
// Create unread notification
$thread = $messageManager->toThread($dto, $messagingUser, $user);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertEquals('message_notification', $jsonData['items'][0]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('read', $jsonData['items'][1]['status']);
self::assertEquals('message_notification', $jsonData['items'][1]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertNull($jsonData['items'][0]['subject']['messageId']);
self::assertNull($jsonData['items'][0]['subject']['threadId']);
self::assertNull($jsonData['items'][0]['subject']['sender']);
self::assertNull($jsonData['items'][0]['subject']['status']);
self::assertNull($jsonData['items'][0]['subject']['createdAt']);
self::assertEquals('This app has not received permission to read your messages.', $jsonData['items'][0]['subject']['body']);
}
public function testApiCanGetNotificationsByStatusAll(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertEquals('message_notification', $jsonData['items'][0]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertSame($message->getId(), $jsonData['items'][0]['subject']['messageId']);
self::assertSame($message->thread->getId(), $jsonData['items'][0]['subject']['threadId']);
self::assertIsArray($jsonData['items'][0]['subject']['sender']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['subject']['sender']);
self::assertSame($messagingUser->getId(), $jsonData['items'][0]['subject']['sender']['userId']);
self::assertEquals('new', $jsonData['items'][0]['subject']['status']);
self::assertNotNull($jsonData['items'][0]['subject']['createdAt']);
self::assertEquals($message->body, $jsonData['items'][0]['subject']['body']);
}
public function testApiCanGetNotificationsFromThreads(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$magazine = $this->getMagazineByName('acme');
$entry = $this->getEntryByTitle('Test notification entry', body: 'Test body', magazine: $magazine, user: $messagingUser);
$userEntry = $this->getEntryByTitle('Test entry', body: 'Test body', magazine: $magazine, user: $user);
$comment = $this->createEntryComment('Test notification comment', $userEntry, $messagingUser);
$commentTwo = $this->createEntryComment('Test notification comment 2', $userEntry, $messagingUser, $comment);
$parent = $this->createEntryComment('Test parent comment', $entry, $user);
$reply = $this->createEntryComment('Test reply comment', $entry, $messagingUser, $parent);
$this->createEntryComment('Test not notified comment', $entry, $messagingUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertEquals('entry_comment_reply_notification', $jsonData['items'][0]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('new', $jsonData['items'][1]['status']);
self::assertEquals('entry_comment_created_notification', $jsonData['items'][1]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertEquals('new', $jsonData['items'][2]['status']);
self::assertEquals('entry_comment_created_notification', $jsonData['items'][2]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][3]);
self::assertEquals('new', $jsonData['items'][3]['status']);
self::assertEquals('entry_created_notification', $jsonData['items'][3]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertSame($reply->getId(), $jsonData['items'][0]['subject']['commentId']);
self::assertIsArray($jsonData['items'][1]['subject']);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]['subject']);
self::assertSame($commentTwo->getId(), $jsonData['items'][1]['subject']['commentId']);
self::assertIsArray($jsonData['items'][2]['subject']);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]['subject']);
self::assertSame($comment->getId(), $jsonData['items'][2]['subject']['commentId']);
self::assertIsArray($jsonData['items'][3]['subject']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $jsonData['items'][3]['subject']);
self::assertSame($entry->getId(), $jsonData['items'][3]['subject']['entryId']);
}
public function testApiCanGetNotificationsFromPosts(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$magazine = $this->getMagazineByName('acme');
$post = $this->createPost('Test notification post', magazine: $magazine, user: $messagingUser);
$userPost = $this->createPost('Test not notified body', magazine: $magazine, user: $user);
$comment = $this->createPostComment('Test notification comment', $userPost, $messagingUser);
$commentTwo = $this->createPostCommentReply('Test notification comment 2', $userPost, $messagingUser, $comment);
$parent = $this->createPostComment('Test parent comment', $post, $user);
$reply = $this->createPostCommentReply('Test reply comment', $post, $messagingUser, $parent);
$this->createPostComment('Test not notified comment', $post, $messagingUser);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read user:message:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/all', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertEquals('post_comment_reply_notification', $jsonData['items'][0]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('new', $jsonData['items'][1]['status']);
self::assertEquals('post_comment_created_notification', $jsonData['items'][1]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertEquals('new', $jsonData['items'][2]['status']);
self::assertEquals('post_comment_created_notification', $jsonData['items'][2]['type']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][3]);
self::assertEquals('new', $jsonData['items'][3]['status']);
self::assertEquals('post_created_notification', $jsonData['items'][3]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertSame($reply->getId(), $jsonData['items'][0]['subject']['commentId']);
self::assertIsArray($jsonData['items'][1]['subject']);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]['subject']);
self::assertSame($commentTwo->getId(), $jsonData['items'][1]['subject']['commentId']);
self::assertIsArray($jsonData['items'][2]['subject']);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]['subject']);
self::assertSame($comment->getId(), $jsonData['items'][2]['subject']['commentId']);
self::assertIsArray($jsonData['items'][3]['subject']);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][3]['subject']);
self::assertSame($post->getId(), $jsonData['items'][3]['subject']['postId']);
}
public function testApiCanGetNotificationsByStatusRead(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$notificationManager = $this->notificationManager;
$notificationManager->readMessageNotification($message, $user);
// Create unread notification
$thread = $messageManager->toThread($dto, $messagingUser, $user);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/read', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('read', $jsonData['items'][0]['status']);
self::assertEquals('message_notification', $jsonData['items'][0]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertNull($jsonData['items'][0]['subject']['messageId']);
self::assertNull($jsonData['items'][0]['subject']['threadId']);
self::assertNull($jsonData['items'][0]['subject']['sender']);
self::assertNull($jsonData['items'][0]['subject']['status']);
self::assertNull($jsonData['items'][0]['subject']['createdAt']);
self::assertEquals('This app has not received permission to read your messages.', $jsonData['items'][0]['subject']['body']);
}
public function testApiCanGetNotificationsByStatusNew(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$notificationManager = $this->notificationManager;
$notificationManager->readMessageNotification($message, $user);
// Create unread notification
$thread = $messageManager->toThread($dto, $messagingUser, $user);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/new', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('new', $jsonData['items'][0]['status']);
self::assertEquals('message_notification', $jsonData['items'][0]['type']);
self::assertIsArray($jsonData['items'][0]['subject']);
self::assertArrayKeysMatch(self::MESSAGE_RESPONSE_KEYS, $jsonData['items'][0]['subject']);
self::assertNull($jsonData['items'][0]['subject']['messageId']);
self::assertNull($jsonData['items'][0]['subject']['threadId']);
self::assertNull($jsonData['items'][0]['subject']['sender']);
self::assertNull($jsonData['items'][0]['subject']['status']);
self::assertNull($jsonData['items'][0]['subject']['createdAt']);
self::assertEquals('This app has not received permission to read your messages.', $jsonData['items'][0]['subject']['body']);
}
public function testApiCannotGetNotificationsByInvalidStatus(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$notificationManager = $this->notificationManager;
$notificationManager->readMessageNotification($message, $user);
// Create unread notification
$thread = $messageManager->toThread($dto, $messagingUser, $user);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/invalid', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCannotGetNotificationCountAnonymous(): void
{
$this->client->request('GET', '/api/notifications/count');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetNotificationCountWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/count', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetNotificationCount(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagingUser = $this->getUserByUsername('JaneDoe');
$magazine = $this->getMagazineByName('acme');
$this->getEntryByTitle('Test notification entry', body: 'Test body', magazine: $magazine, user: $messagingUser);
$this->createPost('Test notification post body', magazine: $magazine, user: $messagingUser);
$messageManager = $this->messageManager;
$dto = new MessageDto();
$dto->body = 'test message';
$thread = $messageManager->toThread($dto, $messagingUser, $user);
/** @var Message $message */
$message = $thread->messages->get(0);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/notifications/count', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(['count'], $jsonData);
self::assertSame(3, $jsonData['count']);
}
public function testApiCannotGetNotificationByIdAnonymous(): void
{
$notification = $this->createMessageNotification();
self::assertNotNull($notification);
$this->client->request('GET', "/api/notification/{$notification->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetNotificationByIdWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$notification = $this->createMessageNotification();
self::assertNotNull($notification);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/notification/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotGetOtherUsersNotificationById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$messagedUser = $this->getUserByUsername('JamesDoe');
$notification = $this->createMessageNotification($messagedUser);
self::assertNotNull($notification);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/notification/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetNotificationById(): void
{
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('JohnDoe');
$notification = $this->createMessageNotification();
self::assertNotNull($notification);
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/notification/{$notification->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::NOTIFICATION_RESPONSE_KEYS, $jsonData);
self::assertSame($notification->getId(), $jsonData['notificationId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Notification/NotificationUpdateApiTest.php
================================================
user = $this->getUserByUsername('user');
$this->client->loginUser($this->user);
self::createOAuth2PublicAuthCodeClient();
$codes = self::getPublicAuthorizationCodeTokenResponse($this->client, scopes: 'read user:notification:edit');
$this->token = $codes['token_type'].' '.$codes['access_token'];
// it seems that the oauth flow detaches the user object from the entity manager, so fetch it again
$this->user = $this->userRepository->findOneByUsername('user');
}
public function testSetEntryNotificationSetting(): void
{
$entry = $this->getEntryByTitle('entry');
$this->testAllSettings("/api/entry/{$entry->getId()}", "/api/notification/update/entry/{$entry->getId()}");
}
public function testSetPostNotificationSetting(): void
{
$post = $this->createPost('post');
$this->testAllSettings("/api/post/{$post->getId()}", "/api/notification/update/post/{$post->getId()}");
}
public function testSetUserNotificationSetting(): void
{
$user2 = $this->getUserByUsername('test');
$this->testAllSettings("/api/users/{$user2->getId()}", "/api/notification/update/user/{$user2->getId()}");
}
public function testSetMagazineNotificationSetting(): void
{
$magazine = $this->getMagazineByName('test');
$this->testAllSettings("/api/magazine/{$magazine->getId()}", "/api/notification/update/magazine/{$magazine->getId()}");
}
private function testAllSettings(string $retrieveUrl, string $updateUrl): void
{
$this->client->request('GET', $retrieveUrl, server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('Default', $jsonData['notificationStatus']);
$this->client->request('PUT', "$updateUrl/Loud", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->request('GET', $retrieveUrl, server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('Loud', $jsonData['notificationStatus']);
$this->client->request('PUT', "$updateUrl/Muted", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->request('GET', $retrieveUrl, server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('Muted', $jsonData['notificationStatus']);
$this->client->request('PUT', "$updateUrl/Default", server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$this->client->request('GET', $retrieveUrl, server: ['HTTP_AUTHORIZATION' => $this->token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertEquals('Default', $jsonData['notificationStatus']);
}
}
================================================
FILE: tests/Functional/Controller/Api/OAuth2/OAuth2ClientApiTest.php
================================================
'/kbin API Created Test Client',
'description' => 'An OAuth2 client for testing purposes, created via the API',
'contactEmail' => 'test@kbin.test',
'redirectUris' => [
'https://localhost:3002',
],
'grants' => [
'authorization_code',
'refresh_token',
],
'scopes' => [
'read',
'write',
'admin:oauth_clients:read',
],
];
$this->client->jsonRequest('POST', '/api/client', $requestData);
self::assertResponseIsSuccessful();
$clientData = self::getJsonResponse($this->client);
self::assertIsArray($clientData);
self::assertArrayKeysMatch(self::CLIENT_RESPONSE_KEYS, $clientData);
self::assertNotNull($clientData['identifier']);
self::assertNotNull($clientData['secret']);
self::assertEquals($requestData['name'], $clientData['name']);
self::assertEquals($requestData['contactEmail'], $clientData['contactEmail']);
self::assertEquals($requestData['description'], $clientData['description']);
self::assertNull($clientData['user']);
self::assertIsArray($clientData['redirectUris']);
self::assertEquals($requestData['redirectUris'], $clientData['redirectUris']);
self::assertIsArray($clientData['grants']);
self::assertEquals($requestData['grants'], $clientData['grants']);
self::assertIsArray($clientData['scopes']);
self::assertEquals($requestData['scopes'], $clientData['scopes']);
self::assertNull($clientData['image']);
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$jsonData = self::getAuthorizationCodeTokenResponse(
$this->client,
clientId: $clientData['identifier'],
clientSecret: $clientData['secret'],
redirectUri: $clientData['redirectUris'][0],
);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
}
public function testApiCanCreateWorkingPublicClient(): void
{
$requestData = [
'name' => '/kbin API Created Test Client',
'description' => 'An OAuth2 client for testing purposes, created via the API',
'contactEmail' => 'test@kbin.test',
'public' => true,
'redirectUris' => [
'https://localhost:3001',
],
'grants' => [
'authorization_code',
'refresh_token',
],
'scopes' => [
'read',
'write',
'admin:oauth_clients:read',
],
];
$this->client->jsonRequest('POST', '/api/client', $requestData);
self::assertResponseIsSuccessful();
$clientData = self::getJsonResponse($this->client);
self::assertIsArray($clientData);
self::assertArrayKeysMatch(self::CLIENT_RESPONSE_KEYS, $clientData);
self::assertNotNull($clientData['identifier']);
self::assertNull($clientData['secret']);
self::assertEquals($requestData['name'], $clientData['name']);
self::assertEquals($requestData['contactEmail'], $clientData['contactEmail']);
self::assertEquals($requestData['description'], $clientData['description']);
self::assertNull($clientData['user']);
self::assertIsArray($clientData['redirectUris']);
self::assertEquals($requestData['redirectUris'], $clientData['redirectUris']);
self::assertIsArray($clientData['grants']);
self::assertEquals($requestData['grants'], $clientData['grants']);
self::assertIsArray($clientData['scopes']);
self::assertEquals($requestData['scopes'], $clientData['scopes']);
self::assertNull($clientData['image']);
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$jsonData = self::getPublicAuthorizationCodeTokenResponse(
$this->client,
clientId: $clientData['identifier'],
redirectUri: $clientData['redirectUris'][0],
);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanCreateWorkingClientWithImage(): void
{
$requestData = [
'name' => '/kbin API Created Test Client',
'description' => 'An OAuth2 client for testing purposes, created via the API',
'contactEmail' => 'test@kbin.test',
'redirectUris' => [
'https://localhost:3002',
],
'grants' => [
'authorization_code',
'refresh_token',
],
'scopes' => [
'read',
'write',
'admin:oauth_clients:read',
],
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request('POST', '/api/client-with-logo', $requestData, files: ['uploadImage' => $image]);
self::assertResponseIsSuccessful();
$clientData = self::getJsonResponse($this->client);
self::assertIsArray($clientData);
self::assertArrayKeysMatch(self::CLIENT_RESPONSE_KEYS, $clientData);
self::assertNotNull($clientData['identifier']);
self::assertNotNull($clientData['secret']);
self::assertEquals($requestData['name'], $clientData['name']);
self::assertEquals($requestData['contactEmail'], $clientData['contactEmail']);
self::assertEquals($requestData['description'], $clientData['description']);
self::assertNull($clientData['user']);
self::assertIsArray($clientData['redirectUris']);
self::assertEquals($requestData['redirectUris'], $clientData['redirectUris']);
self::assertIsArray($clientData['grants']);
self::assertEquals($requestData['grants'], $clientData['grants']);
self::assertIsArray($clientData['scopes']);
self::assertEquals($requestData['scopes'], $clientData['scopes']);
self::assertisArray($clientData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $clientData['image']);
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::runAuthorizationCodeFlowToConsentPage($this->client, 'read write', 'oauth2state', $clientData['identifier'], $clientData['redirectUris'][0]);
self::assertSelectorExists('img.oauth-client-logo');
$logo = $this->client->getCrawler()->filter('img.oauth-client-logo')->first();
self::assertStringContainsString($clientData['image']['filePath'], $logo->attr('src'));
self::runAuthorizationCodeFlowToRedirectUri($this->client, 'read write', 'yes', 'oauth2state', $clientData['identifier'], $clientData['redirectUris'][0]);
$jsonData = self::runAuthorizationCodeTokenFlow($this->client, $clientData['identifier'], $clientData['secret'], $clientData['redirectUris'][0]);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
}
public function testApiCanDeletePrivateClient(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$query = http_build_query([
'client_id' => 'testclient',
'client_secret' => 'testsecret',
]);
$this->client->request('DELETE', '/api/client?'.$query);
self::assertResponseStatusCodeSame(204);
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseStatusCodeSame(401);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_client', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
}
public function testAdminApiCanAccessClientStats(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:oauth_clients:read');
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$query = http_build_query([
'resolution' => 'day',
]);
$this->client->request('GET', '/api/clients/stats?'.$query, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('data', $jsonData);
self::assertIsArray($jsonData['data']);
self::assertCount(1, $jsonData['data']);
self::assertIsArray($jsonData['data'][0]);
self::assertArrayHasKey('client', $jsonData['data'][0]);
self::assertEquals('/kbin Test Client', $jsonData['data'][0]['client']);
self::assertArrayHasKey('datetime', $jsonData['data'][0]);
// If tests are run near midnight UTC we might get unlucky with a failure, but that
// should be unlikely.
$today = (new \DateTime())->setTime(0, 0)->format('Y-m-d H:i:s');
self::assertEquals($today, $jsonData['data'][0]['datetime']);
self::assertArrayHasKey('count', $jsonData['data'][0]);
self::assertEquals(1, $jsonData['data'][0]['count']);
}
public function testAdminApiCannotAccessClientStatsWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$query = http_build_query([
'resolution' => 'day',
]);
$this->client->request('GET', '/api/clients/stats?'.$query, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('type', $jsonData);
self::assertEquals('https://tools.ietf.org/html/rfc2616#section-10', $jsonData['type']);
self::assertArrayHasKey('title', $jsonData);
self::assertEquals('An error occurred', $jsonData['title']);
self::assertArrayHasKey('status', $jsonData);
self::assertEquals(403, $jsonData['status']);
self::assertArrayHasKey('detail', $jsonData);
}
public function testAdminApiCanAccessClientList(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:oauth_clients:read');
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$this->client->request('GET', '/api/clients', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('items', $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayHasKey('identifier', $jsonData['items'][0]);
self::assertArrayNotHasKey('secret', $jsonData['items'][0]);
self::assertEquals('testclient', $jsonData['items'][0]['identifier']);
self::assertArrayHasKey('name', $jsonData['items'][0]);
self::assertEquals('/kbin Test Client', $jsonData['items'][0]['name']);
self::assertArrayHasKey('contactEmail', $jsonData['items'][0]);
self::assertEquals('test@kbin.test', $jsonData['items'][0]['contactEmail']);
self::assertArrayHasKey('description', $jsonData['items'][0]);
self::assertEquals('An OAuth2 client for testing purposes', $jsonData['items'][0]['description']);
self::assertArrayHasKey('user', $jsonData['items'][0]);
self::assertNull($jsonData['items'][0]['user']);
self::assertArrayHasKey('active', $jsonData['items'][0]);
self::assertEquals(true, $jsonData['items'][0]['active']);
self::assertArrayHasKey('createdAt', $jsonData['items'][0]);
self::assertNotNull($jsonData['items'][0]['createdAt']);
self::assertArrayHasKey('redirectUris', $jsonData['items'][0]);
self::assertIsArray($jsonData['items'][0]['redirectUris']);
self::assertCount(1, $jsonData['items'][0]['redirectUris']);
self::assertArrayHasKey('grants', $jsonData['items'][0]);
self::assertIsArray($jsonData['items'][0]['grants']);
self::assertCount(2, $jsonData['items'][0]['grants']);
self::assertArrayHasKey('scopes', $jsonData['items'][0]);
self::assertIsArray($jsonData['items'][0]['scopes']);
self::assertArrayHasKey('pagination', $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayHasKey('count', $jsonData['pagination']);
self::assertEquals(1, $jsonData['pagination']['count']);
self::assertArrayHasKey('currentPage', $jsonData['pagination']);
self::assertEquals(1, $jsonData['pagination']['currentPage']);
self::assertArrayHasKey('maxPage', $jsonData['pagination']);
self::assertEquals(1, $jsonData['pagination']['maxPage']);
self::assertArrayHasKey('perPage', $jsonData['pagination']);
self::assertEquals(15, $jsonData['pagination']['perPage']);
}
public function testAdminApiCannotAccessClientListWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$this->client->request('GET', '/api/clients', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('type', $jsonData);
self::assertEquals('https://tools.ietf.org/html/rfc2616#section-10', $jsonData['type']);
self::assertArrayHasKey('title', $jsonData);
self::assertEquals('An error occurred', $jsonData['title']);
self::assertArrayHasKey('status', $jsonData);
self::assertEquals(403, $jsonData['status']);
self::assertArrayHasKey('detail', $jsonData);
}
public function testAdminApiCanAccessClientByIdentifier(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:oauth_clients:read');
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$this->client->request('GET', '/api/clients/testclient', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('identifier', $jsonData);
self::assertArrayNotHasKey('secret', $jsonData);
self::assertEquals('testclient', $jsonData['identifier']);
self::assertArrayHasKey('name', $jsonData);
self::assertEquals('/kbin Test Client', $jsonData['name']);
self::assertArrayHasKey('contactEmail', $jsonData);
self::assertEquals('test@kbin.test', $jsonData['contactEmail']);
self::assertArrayHasKey('description', $jsonData);
self::assertEquals('An OAuth2 client for testing purposes', $jsonData['description']);
self::assertArrayHasKey('user', $jsonData);
self::assertNull($jsonData['user']);
self::assertArrayHasKey('active', $jsonData);
self::assertEquals(true, $jsonData['active']);
self::assertArrayHasKey('createdAt', $jsonData);
self::assertNotNull($jsonData['createdAt']);
self::assertArrayHasKey('redirectUris', $jsonData);
self::assertIsArray($jsonData['redirectUris']);
self::assertCount(1, $jsonData['redirectUris']);
self::assertArrayHasKey('grants', $jsonData);
self::assertIsArray($jsonData['grants']);
self::assertCount(2, $jsonData['grants']);
self::assertArrayHasKey('scopes', $jsonData);
self::assertIsArray($jsonData['scopes']);
}
public function testApiCanRevokeTokens(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$tokenData = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'admin:oauth_clients:read');
self::assertResponseIsSuccessful();
self::assertIsArray($tokenData);
self::assertArrayHasKey('access_token', $tokenData);
self::assertArrayHasKey('refresh_token', $tokenData);
$token = 'Bearer '.$tokenData['access_token'];
$this->client->request('GET', '/api/clients/testclient', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$this->client->request('POST', '/api/revoke', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$this->client->request('GET', '/api/clients/testclient', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(401);
$jsonData = self::getRefreshTokenResponse($this->client, $tokenData['refresh_token']);
self::assertResponseStatusCodeSame(400);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_grant', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
self::assertEquals('The refresh token is invalid.', $jsonData['error_description']);
self::assertArrayHasKey('hint', $jsonData);
self::assertEquals('Token has been revoked', $jsonData['hint']);
}
public function testAdminApiCannotAccessClientByIdentifierWithoutScope(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', isAdmin: true));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('access_token', $jsonData);
$token = 'Bearer '.$jsonData['access_token'];
$this->client->request('GET', '/api/clients/testclient', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('type', $jsonData);
self::assertEquals('https://tools.ietf.org/html/rfc2616#section-10', $jsonData['type']);
self::assertArrayHasKey('title', $jsonData);
self::assertEquals('An error occurred', $jsonData['title']);
self::assertArrayHasKey('status', $jsonData);
self::assertEquals(403, $jsonData['status']);
self::assertArrayHasKey('detail', $jsonData);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Admin/PostPurgeApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeArticlePostWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminCannotPurgeArticlePost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPurgeArticlePost(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotPurgeImagePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, magazine: $magazine);
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotPurgeImagePostWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user', isAdmin: true);
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonAdminCannotPurgeImagePost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPurgeImagePost(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post/{$post->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/Admin/PostCommentPurgeApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$commentRepository = $this->postCommentRepository;
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge");
self::assertResponseStatusCodeSame(401);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCannotPurgeCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiNonAdminCannotPurgeComment(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $otherUser, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCanPurgeComment(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
public function testApiCannotPurgeImageCommentAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, imageDto: $imageDto);
$commentRepository = $this->postCommentRepository;
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge");
self::assertResponseStatusCodeSame(401);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCannotPurgeImageCommentWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user', isAdmin: true);
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, imageDto: $imageDto);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiNonAdminCannotPurgeImageComment(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, imageDto: $imageDto);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
}
public function testApiCanPurgeImageComment(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, imageDto: $imageDto);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($admin);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:post_comment:purge');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/admin/post-comment/{$comment->getId()}/purge", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/Moderate/PostCommentSetAdultApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post);
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/true");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSetCommentAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotSetCommentAdult(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertTrue($jsonData['isAdult']);
}
public function testApiCannotUnsetCommentAdultAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/false");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUnsetCommentAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotUnsetCommentAdult(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnsetCommentAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$comment->isAdult = true;
$entityManager->persist($comment);
$entityManager->flush();
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertFalse($jsonData['isAdult']);
$comment = $commentRepository->find($comment->getId());
self::assertFalse($comment->isAdult);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/Moderate/PostCommentSetLanguageApiTest.php
================================================
createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/de");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotSetCommentLanguageWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotSetCommentLanguage(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetCommentLanguage(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('de', $jsonData['lang']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/Moderate/PostCommentTrashApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post);
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/trash");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotTrashCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotTrashComment(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanTrashComment(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByName('acme');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('trashed', $jsonData['visibility']);
}
public function testApiCannotRestoreCommentAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post);
$postCommentManager = $this->postCommentManager;
$postCommentManager->trash($this->getUserByUsername('user'), $comment);
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/restore");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRestoreCommentWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$postCommentManager = $this->postCommentManager;
$postCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiNonModCannotRestoreComment(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('a post', $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$postCommentManager = $this->postCommentManager;
$postCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRestoreComment(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user2);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$postCommentManager = $this->postCommentManager;
$postCommentManager->trash($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post_comment:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post-comment/{$comment->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame('test comment', $jsonData['body']);
self::assertSame('visible', $jsonData['visibility']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentCreateApiTest.php
================================================
createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments",
parameters: $comment
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateCommentWithoutScope(): void
{
$post = $this->createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateComment(): void
{
$post = $this->createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['parentId']);
}
public function testApiCannotCreateCommentReplyAnonymous(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply",
parameters: $comment
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateCommentReplyWithoutScope(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateCommentReply(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply",
parameters: $comment, server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertSame($postComment->getId(), $jsonData['rootId']);
self::assertSame($postComment->getId(), $jsonData['parentId']);
}
public function testApiCannotCreateImageCommentAnonymous(): void
{
$post = $this->createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image]
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageCommentWithoutScope(): void
{
$post = $this->createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageComment(): void
{
$post = $this->createPost('a post');
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['parentId']);
}
public function testApiCannotCreateImageCommentReplyAnonymous(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image]
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImageCommentReplyWithoutScope(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.tmp');
$image = new UploadedFile($tmpPath.'.tmp', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImageCommentReply(): void
{
$post = $this->createPost('a post');
$postComment = $this->createPostComment('a comment', $post);
$comment = [
'body' => 'Test comment',
'lang' => 'en',
'isAdult' => false,
'alt' => 'It\'s Kibby!',
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/posts/{$post->getId()}/comments/{$postComment->getId()}/reply/image",
parameters: $comment, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment['body'], $jsonData['body']);
self::assertSame($comment['lang'], $jsonData['lang']);
self::assertSame($comment['isAdult'], $jsonData['isAdult']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertSame($postComment->getId(), $jsonData['rootId']);
self::assertSame($postComment->getId(), $jsonData['parentId']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertEquals($expectedPath, $jsonData['image']['filePath']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentDeleteApiTest.php
================================================
createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$this->client->request('DELETE', "/api/post-comments/{$comment->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user2);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNull($comment);
}
public function testApiCanSoftDeleteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$this->createPostComment('test comment', $post, $user, parent: $comment);
$commentRepository = $this->postCommentRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$comment = $commentRepository->find($comment->getId());
self::assertNotNull($comment);
self::assertTrue($comment->isSoftDeleted());
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentReportApiTest.php
================================================
createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$report = [
'reason' => 'This comment breaks the rules!',
];
$this->client->jsonRequest('POST', "/api/post-comments/{$comment->getId()}/report", $report);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotReportCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/post-comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanReportOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user2);
$reportRepository = $this->reportRepository;
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/post-comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$report = $reportRepository->findBySubject($comment);
self::assertNotNull($report);
self::assertSame('This comment breaks the rules!', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
}
public function testApiCanReportOwnComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$reportRepository = $this->reportRepository;
$report = [
'reason' => 'This comment breaks the rules!',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/post-comments/{$comment->getId()}/report", $report, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$report = $reportRepository->findBySubject($comment);
self::assertNotNull($report);
self::assertSame('This comment breaks the rules!', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentRetrieveApiTest.php
================================================
createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i}", $post);
}
$this->client->request('GET', "/api/posts/{$post->getId()}/comments");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertNull($comment['bookmarks']);
}
}
public function testApiCannotGetPostCommentsByPreferredLangAnonymous(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i}", $post);
}
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?usePreferredLangs=true");
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetPostCommentsByPreferredLang(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i}", $post);
$this->createPostComment("test german parent comment {$i}", $post, lang: 'de');
$this->createPostComment("test dutch parent comment {$i}", $post, lang: 'nl');
}
self::createOAuth2AuthCodeClient();
$user = $this->getUserByUsername('user');
$user->preferredLanguages = ['en', 'de'];
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?usePreferredLangs=true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetPostCommentsWithLanguageAnonymous(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i}", $post);
$this->createPostComment("test german parent comment {$i}", $post, lang: 'de');
$this->createPostComment("test dutch comment {$i}", $post, lang: 'nl');
}
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?lang[]=en&lang[]=de");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertNull($comment['bookmarks']);
}
}
public function testApiCanGetPostCommentsWithLanguage(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i}", $post);
$this->createPostComment("test german parent comment {$i}", $post, lang: 'de');
$this->createPostComment("test dutch parent comment {$i}", $post, lang: 'nl');
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?lang[]=en&lang[]=de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(10, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('parent comment', $comment['body']);
self::assertTrue('en' === $comment['lang'] || 'de' === $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetPostComments(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$this->createPostComment("test parent comment {$i} #tag @user", $post);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(0, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertSame(['@user'], $comment['mentions']);
self::assertIsArray($comment['tags']);
self::assertSame(['tag'], $comment['tags']);
self::assertIsArray($comment['children']);
self::assertEmpty($comment['children']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetPostCommentsWithChildren(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 5; ++$i) {
$comment = $this->createPostComment("test parent comment {$i}", $post);
$this->createPostComment("test child comment {$i}", $post, parent: $comment);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(5, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(5, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(1, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertCount(1, $comment['children']);
self::assertIsArray($comment['children'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment['children'][0]);
self::assertStringContainsString('test child comment', $comment['children'][0]['body']);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetPostCommentsLimitedDepth(): void
{
$post = $this->createPost('test post');
for ($i = 0; $i < 2; ++$i) {
$comment = $this->createPostComment("test parent comment {$i}", $post);
$parent = $comment;
for ($j = 1; $j <= 5; ++$j) {
$parent = $this->createPostComment("test child comment {$i} depth {$j}", $post, parent: $parent);
}
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?d=3", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertIsArray($comment['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $comment['user']);
self::assertIsArray($comment['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $comment['magazine']);
self::assertSame($post->getId(), $comment['postId']);
self::assertStringContainsString('test parent comment', $comment['body']);
self::assertSame('en', $comment['lang']);
self::assertSame(0, $comment['uv']);
self::assertSame(0, $comment['favourites']);
self::assertSame(5, $comment['childCount']);
self::assertSame('visible', $comment['visibility']);
self::assertIsArray($comment['mentions']);
self::assertEmpty($comment['mentions']);
self::assertIsArray($comment['children']);
self::assertCount(1, $comment['children']);
$depth = 0;
$current = $comment;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $current['children'][0]);
self::assertStringContainsString('test child comment', $current['children'][0]['body']);
self::assertSame(5 - ($depth + 1), $current['children'][0]['childCount']);
$current = $current['children'][0];
++$depth;
}
self::assertSame(3, $depth);
self::assertFalse($comment['isAdult']);
self::assertNull($comment['image']);
self::assertNull($comment['parentId']);
self::assertNull($comment['rootId']);
// No scope granted so these should be null
self::assertNull($comment['isFavourited']);
self::assertNull($comment['userVote']);
self::assertNull($comment['apId']);
self::assertEmpty($comment['tags']);
self::assertNull($comment['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $comment['lastActive'], 'lastActive date format invalid');
self::assertIsArray($comment['bookmarks']);
self::assertEmpty($comment['bookmarks']);
}
}
public function testApiCanGetPostCommentsNewest(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetPostCommentsOldest(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetPostCommentsActive(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetPostCommentsHot(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sort=hot", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetPostCommentByIdAnonymous(): void
{
$post = $this->createPost('test post');
$comment = $this->createPostComment('test parent comment', $post);
$this->client->request('GET', "/api/post-comments/{$comment->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertEmpty($jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['bookmarks']);
}
public function testApiCanGetPostCommentById(): void
{
$post = $this->createPost('test post');
$comment = $this->createPostComment('test parent comment', $post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/post-comments/{$comment->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertEmpty($jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
// No scope granted so these should be null
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetPostCommentByIdWithDepth(): void
{
$post = $this->createPost('test post');
$comment = $this->createPostComment('test parent comment', $post);
$parent = $comment;
for ($i = 0; $i < 5; ++$i) {
$parent = $this->createPostComment('test nested reply', $post, parent: $parent);
}
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/post-comments/{$comment->getId()}?d=2", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertStringContainsString('test parent comment', $jsonData['body']);
self::assertSame('en', $jsonData['lang']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(5, $jsonData['childCount']);
self::assertSame('visible', $jsonData['visibility']);
self::assertIsArray($jsonData['mentions']);
self::assertEmpty($jsonData['mentions']);
self::assertIsArray($jsonData['children']);
self::assertCount(1, $jsonData['children']);
self::assertFalse($jsonData['isAdult']);
self::assertNull($jsonData['image']);
self::assertNull($jsonData['parentId']);
self::assertNull($jsonData['rootId']);
// No scope granted so these should be null
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertNull($jsonData['apId']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
$depth = 0;
$current = $jsonData;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $current['children'][0]);
++$depth;
$current = $current['children'][0];
}
self::assertSame(2, $depth);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentUpdateApiTest.php
================================================
createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/post-comments/{$comment->getId()}", $update);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post-comments/{$comment->getId()}", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersComment(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('other');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user2);
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post-comments/{$comment->getId()}", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$parent = $comment;
for ($i = 0; $i < 5; ++$i) {
$parent = $this->createPostComment('test reply', $post, $user, parent: $parent);
}
$update = [
'body' => 'updated body',
'lang' => 'de',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post-comments/{$comment->getId()}?d=2", $update, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame($comment->getId(), $jsonData['commentId']);
self::assertSame($update['body'], $jsonData['body']);
self::assertSame($update['lang'], $jsonData['lang']);
self::assertSame($update['isAdult'], $jsonData['isAdult']);
self::assertSame(5, $jsonData['childCount']);
$depth = 0;
$current = $jsonData;
while (\count($current['children']) > 0) {
self::assertIsArray($current['children'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $current['children'][0]);
++$depth;
$current = $current['children'][0];
}
self::assertSame(2, $depth);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentVoteApiTest.php
================================================
createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpvoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpvoteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(1, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(1, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
public function testApiCannotDownvoteCommentAnonymous(): void
{
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/-1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDownvoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDownvoteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCannotRemoveVoteCommentAnonymous(): void
{
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $this->getUserByUsername('user'), rateLimit: false);
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/0");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotRemoveVoteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRemoveVoteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$voteManager = $this->voteManager;
$voteManager->vote(1, $comment, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
public function testApiCannotFavouriteCommentAnonymous(): void
{
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post);
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/favourite");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotFavouriteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanFavouriteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(1, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertTrue($jsonData['isFavourited']);
}
public function testApiCannotUnfavouriteCommentWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnfavouriteComment(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$comment = $this->createPostComment('test comment', $post, $user);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $comment);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post_comment:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('PUT', "/api/post-comments/{$comment->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isFavourited']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/PostCommentsActivityApiTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $user);
$this->client->jsonRequest('GET', "/api/post-comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
}
public function testUpvotes()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $author, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $author);
$this->favouriteManager->toggle($user1, $comment);
$this->favouriteManager->toggle($user2, $comment);
$this->client->jsonRequest('GET', "/api/post-comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['upvotes']);
self::assertTrue(array_all($jsonData['upvotes'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['upvotes']));
}
public function testBoosts()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $author, magazine: $magazine);
$comment = $this->createPostComment('test comment', $post, $author);
$this->voteManager->upvote($comment, $user1);
$this->voteManager->upvote($comment, $user2);
$this->client->jsonRequest('GET', "/api/post-comments/{$comment->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['boosts']);
self::assertTrue(array_all($jsonData['boosts'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['boosts']));
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Comment/UserPostCommentRetrieveApiTest.php
================================================
createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$user = $post->user;
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
}
public function testApiCanGetUserPostComments(): void
{
$this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$user = $post->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
}
public function testApiCanGetUserPostCommentsDepth(): void
{
$this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$nested1 = $this->createPostComment('test comment nested 1', $post, parent: $comment);
$nested2 = $this->createPostComment('test comment nested 2', $post, parent: $nested1);
$nested3 = $this->createPostComment('test comment nested 3', $post, parent: $nested2);
$user = $post->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments?d=2", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(4, $jsonData['pagination']['count']);
foreach ($jsonData['items'] as $comment) {
self::assertIsArray($comment);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $comment);
self::assertTrue(\count($comment['children']) <= 1);
$depth = 0;
$current = $comment;
while (\count($current['children']) > 0) {
++$depth;
$current = $current['children'][0];
self::assertIsArray($current);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $current);
}
self::assertTrue($depth <= 2);
}
}
public function testApiCanGetUserPostCommentsNewest(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$user = $post->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserPostCommentsOldest(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$user = $post->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserPostCommentsActive(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$user = $post->user;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['commentId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['commentId']);
}
public function testApiCanGetUserPostCommentsHot(): void
{
$post = $this->createPost('post');
$first = $this->createPostComment('first', $post);
$second = $this->createPostComment('second', $post);
$third = $this->createPostComment('third', $post);
$user = $post->user;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments?sort=hot", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['commentId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['commentId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['commentId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetUserPostCommentsWithUserVoteStatus(): void
{
$this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine);
$comment = $this->createPostComment('test comment', $post);
$user = $post->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$user->getId()}/post-comments", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($comment->getId(), $jsonData['items'][0]['commentId']);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('test comment', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(0, $jsonData['items'][0]['childCount']);
self::assertIsArray($jsonData['items'][0]['children']);
self::assertEmpty($jsonData['items'][0]['children']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/MagazinePostRetrieveApiTest.php
================================================
createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['comments']);
}
public function testApiCanGetMagazinePosts(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['comments']);
}
public function testApiCanGetMagazinePostsPinnedFirst(): void
{
$voteManager = $this->voteManager;
$postManager = $this->postManager;
$voter = $this->getUserByUsername('voter');
$first = $this->createPost('a post');
$this->createPostComment('up the ranking', $first);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->createPost('another post', magazine: $magazine);
// Upvote and comment on $second so it should come first, but then pin $third so it actually comes first
$voteManager->vote(1, $second, $voter, rateLimit: false);
$this->createPostComment('test', $second, $voter);
$third = $this->createPost('a pinned post', magazine: $magazine);
$postManager->pin($third);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('a pinned post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertTrue($jsonData['items'][0]['isPinned']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertSame(1, $jsonData['items'][1]['comments']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertFalse($jsonData['items'][1]['isPinned']);
}
public function testApiCanGetMagazinePostsNewest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$magazine = $first->magazine;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetMagazinePostsOldest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$magazine = $first->magazine;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetMagazinePostsCommented(): void
{
$first = $this->createPost('first');
$this->createPostComment('comment 1', $first);
$this->createPostComment('comment 2', $first);
$second = $this->createPost('second');
$this->createPostComment('comment 1', $second);
$third = $this->createPost('third');
$magazine = $first->magazine;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts?sort=commented", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['comments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['comments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['comments']);
}
public function testApiCanGetMagazinePostsActive(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$magazine = $first->magazine;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetMagazinePostsTop(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$magazine = $first->magazine;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetMagazinePostsWithUserVoteStatus(): void
{
$first = $this->createPost('an post');
$this->createPostComment('up the ranking', $first);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/magazine/{$magazine->getId()}/posts", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('another-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Moderate/PostLockApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotLockPost(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user2, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotLockPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanLockPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertTrue($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiAuthorNonModeratorCanLockPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertTrue($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUnlockPostAnonymous(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$postManager = $this->postManager;
$postManager->toggleLock($post, $user);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotUnpinPost(): void
{
$user = $this->getUserByUsername('user');
$user2 = $this->getUserByUsername('user2');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user2, magazine: $magazine);
$postManager = $this->postManager;
$postManager->toggleLock($post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnpinPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->toggleLock($post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnpinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->toggleLock($post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertFalse($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiAuthorNonModeratorCanUnpinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->toggleLock($post, $user);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:lock');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/lock", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertFalse($jsonData['isLocked']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Moderate/PostPinApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotPinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotPinPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanPinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertTrue($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUnpinPostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$postManager = $this->postManager;
$postManager->pin($post);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotUnpinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->pin($post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnpinPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->pin($post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUnpinPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->pin($post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:pin');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/pin", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Moderate/PostSetAdultApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/true");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetPostAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetPostAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetPostAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/true", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotSetPostNotAdultAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$entityManager = $this->entityManager;
$post->isAdult = true;
$entityManager->persist($post);
$entityManager->flush();
$this->client->request('PUT', "/api/moderate/post/{$post->getId()}/adult/false");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetPostNotAdult(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$entityManager = $this->entityManager;
$post->isAdult = true;
$entityManager->persist($post);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetPostNotAdultWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$entityManager = $this->entityManager;
$post->isAdult = true;
$entityManager->persist($post);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanSetPostNotAdult(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$entityManager = $this->entityManager;
$post->isAdult = true;
$entityManager->persist($post);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:set_adult');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/adult/false", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('test-article', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Moderate/PostSetLanguageApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/de");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotSetPostLanguage(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetPostLanguageWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotSetPostLanguageInvalid(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/fake", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/ac", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/aaa", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/a", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCanSetPostLanguage(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/de", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('de', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCanSetPostLanguage3Letter(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:language');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/elx", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('elx', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/Moderate/PostTrashApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/trash");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotTrashPost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotTrashPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanTrashPost(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/trash", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('trashed', $jsonData['visibility']);
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotRestorePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$post = $this->createPost('test post', magazine: $magazine);
$postManager = $this->postManager;
$postManager->trash($user, $post);
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/restore");
self::assertResponseStatusCodeSame(401);
}
public function testApiNonModeratorCannotRestorePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->trash($user, $post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRestorePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme', $user);
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$postManager = $this->postManager;
$postManager->trash($user, $post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRestorePost(): void
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$magazineManager = $this->magazineManager;
$moderator = new ModeratorDto($magazine);
$moderator->user = $user;
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$postManager = $this->postManager;
$postManager->trash($user, $post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post:trash');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/moderate/post/{$post->getId()}/restore", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('visible', $jsonData['visibility']);
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostCreateApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$postRequest = [
'body' => 'This is a microblog',
'lang' => 'en',
'isAdult' => false,
];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/posts", parameters: $postRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreatePostWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'body' => 'No scope post',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/posts", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreatePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'body' => 'This is a microblog #test @user',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/posts", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals('This is a microblog #test @user', $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['test'], $jsonData['tags']);
self::assertIsArray($jsonData['mentions']);
self::assertSame(['@user'], $jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['apId']);
self::assertEquals('This-is-a-microblog-test-at-user', $jsonData['slug']);
}
public function testApiCannotCreateImagePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'alt' => 'It\'s kibby!',
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/posts/image",
parameters: $postRequest, files: ['uploadImage' => $image],
);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotCreateImagePostWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'alt' => 'It\'s kibby!',
'lang' => 'en',
'isAdult' => false,
];
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/posts/image",
parameters: $postRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanCreateImagePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'alt' => 'It\'s kibby!',
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request(
'POST', "/api/magazine/{$magazine->getId()}/posts/image",
parameters: $postRequest, files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $token]
);
self::assertResponseStatusCodeSame(201);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertNotNull($jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals('', $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($expectedPath, $jsonData['image']['filePath']);
self::assertEquals('It\'s kibby!', $jsonData['image']['altText']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertNull($jsonData['apId']);
self::assertEquals('acme-It-s-kibby', $jsonData['slug']);
}
public function testApiCannotCreatePostWithoutMagazine(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$invalidId = $magazine->getId() + 1;
$postRequest = [
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$invalidId}/posts", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
$this->client->request('POST', "/api/magazine/{$invalidId}/posts/image", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(404);
}
public function testApiCannotCreatePostWithoutBodyOrImage(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$postRequest = [
'lang' => 'en',
'isAdult' => false,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:create');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/magazine/{$magazine->getId()}/posts", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
$this->client->request('POST', "/api/magazine/{$magazine->getId()}/posts/image", parameters: $postRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostDeleteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost(body: 'test for deletion', magazine: $magazine);
$this->client->request('DELETE', "/api/post/{$post->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeletePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost(body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersPost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost(body: 'test for deletion', user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeletePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost(body: 'test for deletion', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
public function testApiCannotDeleteImagePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, magazine: $magazine);
$this->client->request('DELETE', "/api/post/{$post->getId()}");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDeleteImagePostWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteOtherUsersImagePost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $otherUser, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanDeleteImagePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:delete');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('DELETE', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostFavouriteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for favourite', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/favourite");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotFavouritePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanFavouritePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for favourite', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/favourite", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(1, $jsonData['favourites']);
self::assertTrue($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-for-favourite', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostReportApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for report', magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
$this->client->jsonRequest('POST', "/api/post/{$post->getId()}/report", $reportRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotReportPostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for report', user: $user, magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/post/{$post->getId()}/report", $reportRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanReportPost(): void
{
$user = $this->getUserByUsername('user');
$otherUser = $this->getUserByUsername('somebody');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test for report', user: $otherUser, magazine: $magazine);
$reportRequest = [
'reason' => 'Test reporting',
];
$magazineRepository = $this->magazineRepository;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:report');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('POST', "/api/post/{$post->getId()}/report", $reportRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(204);
$magazine = $magazineRepository->find($magazine->getId());
$reports = $magazineRepository->findReports($magazine);
self::assertSame(1, $reports->count());
/** @var Report $report */
$report = $reports->getCurrentPageResults()[0];
self::assertEquals('Test reporting', $report->reason);
self::assertSame($user->getId(), $report->reporting->getId());
self::assertSame($otherUser->getId(), $report->reported->getId());
self::assertSame($post->getId(), $report->getSubject()->getId());
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostRetrieveApiTest.php
================================================
client->request('GET', '/api/posts/subscribed');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetSubscribedPostsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'write');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetSubscribedPosts(): void
{
$user = $this->getUserByUsername('user');
$this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag', $user);
$post = $this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/subscribed', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('another-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCanGetSubscribedPostsWithBoosts(): void
{
$user = $this->getUserByUsername('user');
$userFollowing = $this->getUserByUsername('user2');
$user3 = $this->getUserByUsername('user3');
$this->userManager->follow($user, $userFollowing, false);
$postFollowed = $this->createPost('a post', user: $userFollowing);
$postBoosted = $this->createPost('third user post', user: $user3);
$this->createPost('unrelated post', user: $user3);
$commentFollowed = $this->createPostComment('a comment', $postBoosted, $userFollowing);
$commentBoosted = $this->createPostComment('a boosted comment', $postBoosted, $user3);
$this->createPostComment('unrelated comment', $postBoosted, $user3);
$this->voteManager->upvote($postBoosted, $userFollowing);
$this->voteManager->upvote($commentBoosted, $userFollowing);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/subscribedWithBoosts', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(4, $jsonData['pagination']['count']);
$retrievedPostIds = array_map(function ($item) {
if (null !== $item['post']) {
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $item['post']);
return $item['post']['postId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedPostIds = array_filter($retrievedPostIds, function ($item) { return null !== $item; });
sort($retrievedPostIds);
$retrievedPostCommentIds = array_map(function ($item) {
if (null !== $item['postComment']) {
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $item['postComment']);
return $item['postComment']['commentId'];
} else {
return null;
}
}, $jsonData['items']);
$retrievedPostCommentIds = array_filter($retrievedPostCommentIds, function ($item) { return null !== $item; });
sort($retrievedPostCommentIds);
$expectedPostIds = [$postFollowed->getId(), $postBoosted->getId()];
sort($expectedPostIds);
$expectedPostCommentIds = [$commentFollowed->getId(), $commentBoosted->getId()];
sort($expectedPostCommentIds);
self::assertEquals($retrievedPostIds, $expectedPostIds);
self::assertEquals($expectedPostCommentIds, $expectedPostCommentIds);
}
public function testApiCannotGetModeratedPostsAnonymous(): void
{
$this->client->request('GET', '/api/posts/moderated');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetModeratedPostsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetModeratedPosts(): void
{
$user = $this->getUserByUsername('user');
$this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag', $user);
$post = $this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read moderate:post');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/moderated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('another-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCannotGetFavouritedPostsAnonymous(): void
{
$this->client->request('GET', '/api/posts/favourited');
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotGetFavouritedPostsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/favourited', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetFavouritedPosts(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('a post');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($user, $post);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts/favourited', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(1, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertTrue($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
}
public function testApiCanGetPostsAnonymous(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->createPost('another post', magazine: $magazine);
// Check that pinned posts don't get pinned to the top of the instance, just the magazine
$postManager = $this->postManager;
$postManager->pin($second);
$this->client->request('GET', '/api/posts');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCanGetPosts(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCanGetPostsWithLanguageAnonymous(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->createPost('another post', magazine: $magazine, lang: 'de');
$this->createPost('a dutch post', magazine: $magazine, lang: 'nl');
// Check that pinned posts don't get pinned to the top of the instance, just the magazine
$postManager = $this->postManager;
$postManager->pin($second);
$this->client->request('GET', '/api/posts?lang[]=en&lang[]=de');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertNull($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('de', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCanGetPostsWithLanguage(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine, lang: 'de');
$this->createPost('a dutch post', magazine: $magazine, lang: 'nl');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?lang[]=en&lang[]=de', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('de', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCannotGetPostsByPreferredLangAnonymous(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$second = $this->createPost('another post', magazine: $magazine);
// Check that pinned posts don't get pinned to the top of the instance, just the magazine
$postManager = $this->postManager;
$postManager->pin($second);
$this->client->request('GET', '/api/posts?usePreferredLangs=true');
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetPostsByPreferredLang(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
$this->createPost('German post', lang: 'de');
$user = $this->getUserByUsername('user');
$user->preferredLanguages = ['en'];
$entityManager = $this->entityManager;
$entityManager->persist($user);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?usePreferredLangs=true', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['items'][0]['isFavourited']);
self::assertNull($jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertEquals('en', $jsonData['items'][1]['lang']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCanGetPostsNewest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?sort=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetPostsOldest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?sort=oldest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetPostsCommented(): void
{
$first = $this->createPost('first');
$this->createPostComment('comment 1', $first);
$this->createPostComment('comment 2', $first);
$second = $this->createPost('second');
$this->createPostComment('comment 1', $second);
$third = $this->createPost('third');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?sort=commented', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['comments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['comments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['comments']);
}
public function testApiCanGetPostsActive(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?sort=active', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetPostsTop(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?sort=top', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetPostsWithUserVoteStatus(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$this->createPost('another post', magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('a post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertSame(1, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
self::assertIsArray($jsonData['items'][0]['bookmarks']);
self::assertEmpty($jsonData['items'][0]['bookmarks']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertEquals('another post', $jsonData['items'][1]['body']);
self::assertIsArray($jsonData['items'][1]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][1]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][1]['magazine']['magazineId']);
self::assertSame(0, $jsonData['items'][1]['comments']);
}
public function testApiCanGetPostByIdAnonymous(): void
{
$post = $this->createPost('a post');
$this->client->request('GET', "/api/post/{$post->getId()}");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertEquals('a post', $jsonData['body']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertNull($jsonData['bookmarks']);
}
public function testApiCanGetPostById(): void
{
$post = $this->createPost('a post');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertEquals('a post', $jsonData['body']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetPostByIdWithUserVoteStatus(): void
{
$post = $this->createPost('a post');
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/post/{$post->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertEquals('a post', $jsonData['body']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertNull($jsonData['image']);
self::assertEquals('en', $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
self::assertFalse($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
// This API creates a view when used
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('a-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
self::assertIsArray($jsonData['bookmarks']);
self::assertEmpty($jsonData['bookmarks']);
}
public function testApiCanGetPostsLocal(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$second->apId = 'https://some.url';
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?federation=local', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
}
public function testApiCanGetPostsFederated(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$second->apId = 'https://some.url';
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', '/api/posts?federation=federated', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($second->getId(), $jsonData['items'][0]['postId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostUpdateApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdatePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersPost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $otherUser, magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdatePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test article', user: $user, magazine: $magazine);
$updateRequest = [
'body' => 'Updated #body @user',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($updateRequest['body'], $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($updateRequest['lang'], $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['body'], $jsonData['tags']);
self::assertIsArray($jsonData['mentions']);
self::assertSame(['@user'], $jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['editedAt'], 'editedAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('Updated-body-at-user', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotUpdateImagePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest);
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpdateImagePostWithoutScope(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$user = $this->getUserByUsername('user');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateOtherUsersImagePost(): void
{
$otherUser = $this->getUserByUsername('somebody');
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $otherUser, magazine: $magazine);
$updateRequest = [
'body' => 'Updated body',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanUpdateImagePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test image', imageDto: $imageDto, user: $user, magazine: $magazine);
$updateRequest = [
'body' => 'Updated #body @user',
'lang' => 'nl',
'isAdult' => true,
];
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}", $updateRequest, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($updateRequest['body'], $jsonData['body']);
self::assertIsArray($jsonData['image']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['image']);
self::assertStringContainsString($imageDto->filePath, $jsonData['image']['filePath']);
self::assertEquals($updateRequest['lang'], $jsonData['lang']);
self::assertIsArray($jsonData['tags']);
self::assertSame(['body'], $jsonData['tags']);
self::assertIsArray($jsonData['mentions']);
self::assertSame(['@user'], $jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertNull($jsonData['isFavourited']);
self::assertNull($jsonData['userVote']);
self::assertTrue($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['editedAt'], 'editedAt date format invalid');
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('Updated-body-at-user', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostVoteApiTest.php
================================================
getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotUpvotePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpvotePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(1, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertFalse($jsonData['isFavourited']);
self::assertSame(1, $jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
public function testApiCannotDownvotePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/-1");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotDownvotePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDownvotePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/-1", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(400);
}
public function testApiCannotClearVotePostAnonymous(): void
{
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', magazine: $magazine);
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/0");
self::assertResponseStatusCodeSame(401);
}
public function testApiCannotClearVotePostWithoutScope(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$voteManager = $this->voteManager;
$voteManager->vote(1, $post, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanClearVotePost(): void
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$voteManager = $this->voteManager;
$voteManager->vote(1, $post, $user, rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($user);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read post:vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('PUT', "/api/post/{$post->getId()}/vote/0", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData);
self::assertSame($post->getId(), $jsonData['postId']);
self::assertIsArray($jsonData['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['magazine']);
self::assertSame($magazine->getId(), $jsonData['magazine']['magazineId']);
self::assertIsArray($jsonData['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['user']);
self::assertSame($user->getId(), $jsonData['user']['userId']);
self::assertEquals($post->body, $jsonData['body']);
self::assertNull($jsonData['image']);
self::assertEquals($post->lang, $jsonData['lang']);
self::assertEmpty($jsonData['tags']);
self::assertNull($jsonData['mentions']);
self::assertSame(0, $jsonData['comments']);
self::assertSame(0, $jsonData['uv']);
self::assertSame(0, $jsonData['dv']);
self::assertSame(0, $jsonData['favourites']);
// No scope for seeing votes granted
self::assertFalse($jsonData['isFavourited']);
self::assertSame(0, $jsonData['userVote']);
self::assertFalse($jsonData['isAdult']);
self::assertFalse($jsonData['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['lastActive'], 'lastActive date format invalid');
self::assertEquals('test-post', $jsonData['slug']);
self::assertNull($jsonData['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/PostsActivityApiTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $user, magazine: $magazine);
$this->client->jsonRequest('GET', "/api/post/{$post->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
}
public function testUpvotes()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $author, magazine: $magazine);
$this->favouriteManager->toggle($user1, $post);
$this->favouriteManager->toggle($user2, $post);
$this->client->jsonRequest('GET', "/api/post/{$post->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['boosts']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['upvotes']);
self::assertTrue(array_all($jsonData['upvotes'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['upvotes']));
}
public function testBoosts()
{
$author = $this->getUserByUsername('userA');
$user1 = $this->getUserByUsername('user1');
$user2 = $this->getUserByUsername('user2');
$this->getUserByUsername('user3');
$magazine = $this->getMagazineByNameNoRSAKey('acme');
$post = $this->createPost('test post', user: $author, magazine: $magazine);
$this->voteManager->upvote($post, $user1);
$this->voteManager->upvote($post, $user2);
$this->client->jsonRequest('GET', "/api/post/{$post->getId()}/activity");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(EntriesActivityApiTest::ACTIVITIES_RESPONSE_DTO_KEYS, $jsonData);
self::assertSame([], $jsonData['upvotes']);
self::assertSame(null, $jsonData['downvotes']);
self::assertCount(2, $jsonData['boosts']);
self::assertTrue(array_all($jsonData['boosts'], function ($u) use ($user1, $user2) {
/* @var UserSmallResponseDto $u */
return $u['userId'] === $user1->getId() || $u['userId'] === $user2->getId();
}), serialize($jsonData['boosts']));
}
}
================================================
FILE: tests/Functional/Controller/Api/Post/UserPostRetrieveApiTest.php
================================================
createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$otherUser = $this->getUserByUsername('somebody');
$this->createPost('another post', magazine: $magazine, user: $otherUser);
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertSame(0, $jsonData['items'][0]['comments']);
}
public function testApiCanGetUserEntries(): void
{
$post = $this->createPost('a post');
$this->createPostComment('up the ranking', $post);
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$otherUser = $this->getUserByUsername('somebody');
$this->createPost('another post', magazine: $magazine, user: $otherUser);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertSame(0, $jsonData['items'][0]['comments']);
}
public function testApiCanGetUserEntriesNewest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$otherUser = $first->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts?sort=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetUserEntriesOldest(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$otherUser = $first->user;
$first->createdAt = new \DateTimeImmutable('-1 hour');
$second->createdAt = new \DateTimeImmutable('-1 second');
$third->createdAt = new \DateTimeImmutable();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts?sort=oldest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetUserEntriesCommented(): void
{
$first = $this->createPost('first');
$this->createPostComment('comment 1', $first);
$this->createPostComment('comment 2', $first);
$second = $this->createPost('second');
$this->createPostComment('comment 1', $second);
$third = $this->createPost('third');
$otherUser = $first->user;
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts?sort=commented", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['comments']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['comments']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['comments']);
}
public function testApiCanGetUserEntriesActive(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$otherUser = $first->user;
$first->lastActive = new \DateTime('-1 hour');
$second->lastActive = new \DateTime('-1 second');
$third->lastActive = new \DateTime();
$entityManager = $this->entityManager;
$entityManager->persist($first);
$entityManager->persist($second);
$entityManager->persist($third);
$entityManager->flush();
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts?sort=active", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($third->getId(), $jsonData['items'][0]['postId']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($first->getId(), $jsonData['items'][2]['postId']);
}
public function testApiCanGetUserEntriesTop(): void
{
$first = $this->createPost('first');
$second = $this->createPost('second');
$third = $this->createPost('third');
$otherUser = $first->user;
$voteManager = $this->voteManager;
$voteManager->vote(1, $first, $this->getUserByUsername('voter1'), rateLimit: false);
$voteManager->vote(1, $first, $this->getUserByUsername('voter2'), rateLimit: false);
$voteManager->vote(1, $second, $this->getUserByUsername('voter1'), rateLimit: false);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts?sort=top", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(3, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($first->getId(), $jsonData['items'][0]['postId']);
self::assertSame(2, $jsonData['items'][0]['uv']);
self::assertIsArray($jsonData['items'][1]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][1]);
self::assertSame($second->getId(), $jsonData['items'][1]['postId']);
self::assertSame(1, $jsonData['items'][1]['uv']);
self::assertIsArray($jsonData['items'][2]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][2]);
self::assertSame($third->getId(), $jsonData['items'][2]['postId']);
self::assertSame(0, $jsonData['items'][2]['uv']);
}
public function testApiCanGetUserEntriesWithUserVoteStatus(): void
{
$this->createPost('a post');
$otherUser = $this->getUserByUsername('somebody');
$magazine = $this->getMagazineByNameNoRSAKey('somemag');
$post = $this->createPost('another post', magazine: $magazine, user: $otherUser);
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('user'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read vote');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->request('GET', "/api/users/{$otherUser->getId()}/posts", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($post->getId(), $jsonData['items'][0]['postId']);
self::assertEquals('another post', $jsonData['items'][0]['body']);
self::assertIsArray($jsonData['items'][0]['magazine']);
self::assertArrayKeysMatch(self::MAGAZINE_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['magazine']);
self::assertSame($magazine->getId(), $jsonData['items'][0]['magazine']['magazineId']);
self::assertIsArray($jsonData['items'][0]['user']);
self::assertArrayKeysMatch(self::USER_SMALL_RESPONSE_KEYS, $jsonData['items'][0]['user']);
self::assertSame($otherUser->getId(), $jsonData['items'][0]['user']['userId']);
self::assertNull($jsonData['items'][0]['image']);
self::assertEquals('en', $jsonData['items'][0]['lang']);
self::assertEmpty($jsonData['items'][0]['tags']);
self::assertNull($jsonData['items'][0]['mentions']);
self::assertSame(0, $jsonData['items'][0]['comments']);
self::assertSame(0, $jsonData['items'][0]['uv']);
self::assertSame(0, $jsonData['items'][0]['dv']);
self::assertSame(0, $jsonData['items'][0]['favourites']);
self::assertFalse($jsonData['items'][0]['isFavourited']);
self::assertSame(0, $jsonData['items'][0]['userVote']);
self::assertFalse($jsonData['items'][0]['isAdult']);
self::assertFalse($jsonData['items'][0]['isPinned']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['createdAt'], 'createdAt date format invalid');
self::assertNull($jsonData['items'][0]['editedAt']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $jsonData['items'][0]['lastActive'], 'lastActive date format invalid');
self::assertEquals('another-post', $jsonData['items'][0]['slug']);
self::assertNull($jsonData['items'][0]['apId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/Search/SearchApiTest.php
================================================
someUser = $this->getUserByUsername('JohnDoe2', email: 'jd@test.tld');
$this->someMagazine = $this->getMagazineByName('acme2', $this->someUser);
}
public function setUpRemoteEntities(): void
{
$this->createRemoteEntryInRemoteMagazine($this->remoteMagazine, $this->remoteUser, function (Entry $entry) {
$this->testEntryUrl = 'https://remote.mbin/m/someremotemagazine/t/'.$entry->getId();
});
}
protected function setUpRemoteActors(): void
{
parent::setUpRemoteActors();
$this->remoteUser = $this->getUserByUsername(self::TEST_USER_NAME, addImage: false);
$this->registerActor($this->remoteUser, $this->remoteDomain, true);
$this->remoteMagazine = $this->getMagazineByName(self::TEST_MAGAZINE_NAME);
$this->registerActor($this->remoteMagazine, $this->remoteDomain, true);
}
public function testApiCannotSearchWithNoQuery(): void
{
$this->client->request('GET', '/api/search/v2');
self::assertResponseStatusCodeSame(400);
}
public function testApiCanFindEntryByTitleAnonymous(): void
{
$entry = $this->getEntryByTitle('A test title to search for', magazine: $this->someMagazine, user: $this->someUser);
$this->getEntryByTitle('Cannot find this', magazine: $this->someMagazine, user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q=title');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 1, 0);
self::validateResponseItemData($jsonData['items'][0], 'entry', $entry->getId());
}
public function testApiCanFindContentByBodyAnonymous(): void
{
$entry = $this->getEntryByTitle('A test title to search for', body: 'This is the body we\'re finding', magazine: $this->someMagazine, user: $this->someUser);
$this->getEntryByTitle('Cannot find this', body: 'No keywords here!', magazine: $this->someMagazine, user: $this->someUser);
$post = $this->createPost('Lets get a post with its body in there too!', magazine: $this->someMagazine, user: $this->someUser);
$this->createPost('But not this one.', magazine: $this->someMagazine, user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q=body');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 2, 0);
foreach ($jsonData['items'] as $item) {
if (null !== $item['entry']) {
$type = 'entry';
$id = $entry->getId();
} else {
$type = 'post';
$id = $post->getId();
}
self::validateResponseItemData($item, $type, $id);
}
}
public function testApiCanFindCommentsByBodyAnonymous(): void
{
$entry = $this->getEntryByTitle('Cannot find this', body: 'No keywords here!', magazine: $this->someMagazine, user: $this->someUser);
$post = $this->createPost('But not this one.', magazine: $this->someMagazine, user: $this->someUser);
$entryComment = $this->createEntryComment('Some comment on a thread', $entry, user: $this->someUser);
$postComment = $this->createPostComment('Some comment on a post', $post, user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q=comment');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 2, 0);
foreach ($jsonData['items'] as $item) {
if (null !== $item['entryComment']) {
$type = 'entryComment';
$id = $entryComment->getId();
} else {
$type = 'postComment';
$id = $postComment->getId();
}
self::validateResponseItemData($item, $type, $id);
}
}
public function testApiCannotFindRemoteUserAnonymousWhenOptionSet(): void
{
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', true);
$this->client->request('GET', '/api/search/v2?q='.self::TEST_USER_HANDLE);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 0);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCannotFindRemoteMagazineAnonymousWhenOptionSet(): void
{
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', true);
$this->client->request('GET', '/api/search/v2?q='.self::TEST_MAGAZINE_HANDLE);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 0);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCanFindRemoteUserByHandleAnonymous(): void
{
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', false);
$this->getUserByUsername('test');
$this->client->request('GET', '/api/search/v2?q=@'.self::TEST_USER_HANDLE);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 1);
self::validateResponseItemData($jsonData['apResults'][0], 'user', null, self::TEST_USER_HANDLE, self::TEST_USER_URL);
$this->client->request('GET', '/api/search/v2?q='.self::TEST_USER_HANDLE);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 1, 1);
self::assertSame(self::TEST_USER_URL, $jsonData['items'][0]['user']['apProfileId']);
self::validateResponseItemData($jsonData['apResults'][0], 'user', null, self::TEST_USER_HANDLE, self::TEST_USER_URL);
// Seems like settings can persist in the test environment? Might only be for bare metal setups.
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCanFindRemoteMagazineByHandleAnonymous(): void
{
// Admin user must exist to retrieve a remote magazine since remote mods aren't federated (yet)
$this->getUserByUsername('admin', isAdmin: true);
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', false);
$this->getMagazineByName('testMag', user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q=!'.self::TEST_MAGAZINE_HANDLE);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 1);
self::validateResponseItemData($jsonData['apResults'][0], 'magazine', null, self::TEST_MAGAZINE_HANDLE, self::TEST_MAGAZINE_URL);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCanFindRemoteUserByUrl(): void
{
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', true);
$this->getUserByUsername('test');
$this->client->loginUser($this->localUser);
$this->client->request('GET', '/api/search/v2?q='.urlencode(self::TEST_USER_URL));
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 1);
self::validateResponseItemData($jsonData['apResults'][0], 'user', null, self::TEST_USER_HANDLE, self::TEST_USER_URL);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCanFindRemoteMagazineByUrl(): void
{
$this->getUserByUsername('admin', isAdmin: true);
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', true);
$this->client->loginUser($this->localUser);
$this->getMagazineByName('testMag', user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q='.urlencode(self::TEST_MAGAZINE_URL));
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 1);
self::validateResponseItemData($jsonData['apResults'][0], 'magazine', null, self::TEST_MAGAZINE_HANDLE, self::TEST_MAGAZINE_URL);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
public function testApiCanFindRemotePostByUrl(): void
{
$this->getUserByUsername('admin', isAdmin: true);
$settingsManager = $this->settingsManager;
$value = $settingsManager->get('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN');
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', true);
$this->client->loginUser($this->localUser);
$this->getMagazineByName('testMag', user: $this->someUser);
$this->client->request('GET', '/api/search/v2?q='.urlencode($this->testEntryUrl));
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::validateResponseOuterData($jsonData, 0, 1);
self::validateResponseItemData($jsonData['apResults'][0], 'entry', null, $this->testEntryUrl);
// Seems like settings can persist in the test environment? Might only be for bare metal setups
$settingsManager->set('KBIN_FEDERATED_SEARCH_ONLY_LOGGEDIN', $value);
}
private static function validateResponseOuterData(array $data, int $expectedLength, int $expectedApLength): void
{
self::assertIsArray($data);
self::assertArrayKeysMatch(self::SEARCH_PAGINATED_KEYS, $data);
self::assertIsArray($data['items']);
self::assertCount($expectedLength, $data['items']);
self::assertIsArray($data['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $data['pagination']);
self::assertSame($expectedLength, $data['pagination']['count']);
self::assertIsArray($data['apResults']);
self::assertCount($expectedApLength, $data['apResults']);
}
private static function validateResponseItemData(array $data, string $expectedType, ?int $expectedId = null, ?string $expectedApId = null, ?string $apProfileId = null): void
{
self::assertIsArray($data);
self::assertArrayKeysMatch(self::SEARCH_ITEM_KEYS, $data);
switch ($expectedType) {
case 'entry':
self::assertNotNull($data['entry']);
self::assertNull($data['entryComment']);
self::assertNull($data['post']);
self::assertNull($data['postComment']);
self::assertNull($data['magazine']);
self::assertNull($data['user']);
self::assertArrayKeysMatch(self::ENTRY_RESPONSE_KEYS, $data['entry']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['entry']['entryId']);
} else {
self::assertSame($expectedApId, $data['entry']['apId']);
}
break;
case 'entryComment':
self::assertNotNull($data['entryComment']);
self::assertNull($data['entry']);
self::assertNull($data['post']);
self::assertNull($data['postComment']);
self::assertNull($data['magazine']);
self::assertNull($data['user']);
self::assertArrayKeysMatch(self::ENTRY_COMMENT_RESPONSE_KEYS, $data['entryComment']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['entryComment']['commentId']);
} else {
self::assertSame($expectedApId, $data['entryComment']['apId']);
}
break;
case 'post':
self::assertNotNull($data['post']);
self::assertNull($data['entry']);
self::assertNull($data['entryComment']);
self::assertNull($data['postComment']);
self::assertNull($data['magazine']);
self::assertNull($data['user']);
self::assertArrayKeysMatch(self::POST_RESPONSE_KEYS, $data['post']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['post']['postId']);
} else {
self::assertSame($expectedApId, $data['post']['apId']);
}
break;
case 'postComment':
self::assertNotNull($data['postComment']);
self::assertNull($data['entry']);
self::assertNull($data['entryComment']);
self::assertNull($data['post']);
self::assertNull($data['magazine']);
self::assertNull($data['user']);
self::assertArrayKeysMatch(self::POST_COMMENT_RESPONSE_KEYS, $data['postComment']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['postComment']['commentId']);
} else {
self::assertSame($expectedApId, $data['postComment']['apId']);
}
break;
case 'magazine':
self::assertNotNull($data['magazine']);
self::assertNull($data['entry']);
self::assertNull($data['entryComment']);
self::assertNull($data['post']);
self::assertNull($data['postComment']);
self::assertNull($data['user']);
self::assertArrayKeysMatch(self::MAGAZINE_RESPONSE_KEYS, $data['magazine']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['magazine']['magazineId']);
} else {
self::assertSame($expectedApId, $data['magazine']['apId']);
}
if (null !== $apProfileId) {
self::assertSame($apProfileId, $data['magazine']['apProfileId']);
}
break;
case 'user':
self::assertNotNull($data['user']);
self::assertNull($data['entry']);
self::assertNull($data['entryComment']);
self::assertNull($data['post']);
self::assertNull($data['postComment']);
self::assertNull($data['magazine']);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $data['user']);
if (null !== $expectedId) {
self::assertSame($expectedId, $data['user']['userId']);
} else {
self::assertSame($expectedApId, $data['user']['apId']);
}
if (null !== $apProfileId) {
self::assertSame($apProfileId, $data['user']['apProfileId']);
}
break;
default:
throw new \AssertionError();
}
}
}
================================================
FILE: tests/Functional/Controller/Api/User/Admin/UserBanApiTest.php
================================================
getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/ban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertFalse($bannedUser->isBanned);
}
public function testApiCannotUnbanUserWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/unban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertTrue($bannedUser->isBanned);
}
public function testApiCannotBanUserWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/ban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertFalse($bannedUser->isBanned);
}
public function testApiCannotUnbanUserWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/unban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertTrue($bannedUser->isBanned);
}
public function testApiCanBanUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/ban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isBanned']), $jsonData);
self::assertTrue($jsonData['isBanned']);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertTrue($bannedUser->isBanned);
}
public function testApiCanUnbanUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/unban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isBanned']), $jsonData);
self::assertFalse($jsonData['isBanned']);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertFalse($bannedUser->isBanned);
}
public function testBanApiReturns404IfUserNotFound(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) ($bannedUser->getId() * 10).'/ban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(404);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertFalse($bannedUser->isBanned);
}
public function testUnbanApiReturns404IfUserNotFound(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) ($bannedUser->getId() * 10).'/unban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(404);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertTrue($bannedUser->isBanned);
}
public function testBanApiReturns401IfTokenNotProvided(): void
{
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/ban');
self::assertResponseStatusCodeSame(401);
}
public function testUnbanApiReturns401IfTokenNotProvided(): void
{
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/unban');
self::assertResponseStatusCodeSame(401);
}
public function testBanApiIsIdempotent(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
// Ban user a second time with the API
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/ban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isBanned']), $jsonData);
self::assertTrue($jsonData['isBanned']);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertTrue($bannedUser->isBanned);
}
public function testUnbanApiIsIdempotent(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
// Do not ban user
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('POST', '/api/admin/users/'.(string) $bannedUser->getId().'/unban', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isBanned']), $jsonData);
self::assertFalse($jsonData['isBanned']);
$repository = $this->userRepository;
$bannedUser = $repository->find($bannedUser->getId());
self::assertFalse($bannedUser->isBanned);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/Admin/UserDeleteApiTest.php
================================================
getUserByUsername('UserWithoutAbout', isAdmin: true);
$deletedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request(
'DELETE',
'/api/admin/users/'.(string) $deletedUser->getId().'/delete_account',
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$deletedUser = $repository->find($deletedUser->getId());
self::assertFalse($deletedUser->isAccountDeleted());
}
public function testApiCannotDeleteUserWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$deletedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:delete');
$this->client->request(
'DELETE',
'/api/admin/users/'.(string) $deletedUser->getId().'/delete_account',
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$deletedUser = $repository->find($deletedUser->getId());
self::assertFalse($deletedUser->isAccountDeleted());
}
public function testApiCanDeleteUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$deletedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:delete');
$this->client->request(
'DELETE',
'/api/admin/users/'.(string) $deletedUser->getId().'/delete_account',
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
$repository = $this->userRepository;
$deletedUser = $repository->find($deletedUser->getId());
self::assertTrue($deletedUser->isAccountDeleted());
}
public function testDeleteApiReturns404IfUserNotFound(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$deletedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:delete');
$this->client->request(
'DELETE',
'/api/admin/users/'.(string) ($deletedUser->getId() * 10).'/delete_account',
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(404);
$repository = $this->userRepository;
$deletedUser = $repository->find($deletedUser->getId());
self::assertFalse($deletedUser->isBanned);
}
public function testDeleteApiReturns401IfTokenNotProvided(): void
{
$deletedUser = $this->getUserByUsername('JohnDoe');
$this->client->request('DELETE', '/api/admin/users/'.(string) $deletedUser->getId().'/delete_account');
self::assertResponseStatusCodeSame(401);
}
public function testDeleteApiIsNotIdempotent(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$deletedUser = $this->getUserByUsername('JohnDoe');
$deleteId = $deletedUser->getId();
$this->userManager->delete($deletedUser);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:delete');
// Ban user a second time with the API
$this->client->request(
'DELETE',
'/api/admin/users/'.(string) $deleteId.'/delete_account',
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(404);
$repository = $this->userRepository;
$deletedUser = $repository->find($deleteId);
self::assertNull($deletedUser);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/Admin/UserPurgeApiTest.php
================================================
getUserByUsername('UserWithoutAbout', isAdmin: true);
$purgedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('DELETE', '/api/admin/users/'.(string) $purgedUser->getId().'/purge_account', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$purgedUser = $repository->find($purgedUser->getId());
self::assertNotNull($purgedUser);
}
public function testApiCannotPurgeUserWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$purgedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:purge');
$this->client->request('DELETE', '/api/admin/users/'.(string) $purgedUser->getId().'/purge_account', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$purgedUser = $repository->find($purgedUser->getId());
self::assertNotNull($purgedUser);
}
public function testApiCanPurgeUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$purgedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:purge');
$this->client->request('DELETE', '/api/admin/users/'.(string) $purgedUser->getId().'/purge_account', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(204);
$repository = $this->userRepository;
$purgedUser = $repository->find($purgedUser->getId());
self::assertNull($purgedUser);
}
public function testPurgeApiReturns404IfUserNotFound(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$purgedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:purge');
$this->client->request('DELETE', '/api/admin/users/'.(string) ($purgedUser->getId() * 10).'/purge_account', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(404);
$repository = $this->userRepository;
$purgedUser = $repository->find($purgedUser->getId());
self::assertNotNull($purgedUser);
}
public function testPurgeApiReturns401IfTokenNotProvided(): void
{
$purgedUser = $this->getUserByUsername('JohnDoe');
$this->client->request('DELETE', '/api/admin/users/'.(string) $purgedUser->getId().'/purge_account');
self::assertResponseStatusCodeSame(401);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/Admin/UserRetrieveBannedApiTest.php
================================================
getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/admin/users/banned', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotRetrieveBannedUsersWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('GET', '/api/admin/users/banned', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveBannedUsers(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$bannedUser = $this->getUserByUsername('JohnDoe');
$this->userManager->ban($bannedUser, $testUser, null);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:ban');
$this->client->request('GET', '/api/admin/users/banned', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isBanned']), $jsonData['items'][0]);
self::assertSame($bannedUser->getId(), $jsonData['items'][0]['userId']);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/Admin/UserVerifyApiTest.php
================================================
getUserByUsername('UserWithoutAbout', isAdmin: true);
$unverifiedUser = $this->getUserByUsername('JohnDoe', active: false);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('PUT', '/api/admin/users/'.(string) $unverifiedUser->getId().'/verify', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$unverifiedUser = $repository->find($unverifiedUser->getId());
self::assertFalse($unverifiedUser->isVerified);
}
public function testApiCannotVerifyUserWithoutAdminAccount(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: false);
$unverifiedUser = $this->getUserByUsername('JohnDoe', active: false);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:verify');
$this->client->request('PUT', '/api/admin/users/'.(string) $unverifiedUser->getId().'/verify', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
$repository = $this->userRepository;
$unverifiedUser = $repository->find($unverifiedUser->getId());
self::assertFalse($unverifiedUser->isVerified);
}
public function testApiCanVerifyUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$unverifiedUser = $this->getUserByUsername('JohnDoe', active: false);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:verify');
$this->client->request('PUT', '/api/admin/users/'.(string) $unverifiedUser->getId().'/verify', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(200);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(array_merge(self::USER_RESPONSE_KEYS, ['isVerified']), $jsonData);
self::assertTrue($jsonData['isVerified']);
$repository = $this->userRepository;
$unverifiedUser = $repository->find($unverifiedUser->getId());
self::assertTrue($unverifiedUser->isVerified);
}
public function testVerifyApiReturns404IfUserNotFound(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout', isAdmin: true);
$unverifiedUser = $this->getUserByUsername('JohnDoe', active: false);
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read admin:user:verify');
$this->client->request('PUT', '/api/admin/users/'.(string) ($unverifiedUser->getId() * 10).'/verify', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(404);
}
public function testVerifyApiReturns401IfTokenNotProvided(): void
{
$unverifiedUser = $this->getUserByUsername('JohnDoe', active: false);
$this->client->request('PUT', '/api/admin/users/'.(string) $unverifiedUser->getId().'/verify');
self::assertResponseStatusCodeSame(401);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserBlockApiTest.php
================================================
getUserByUsername('UserWithoutAbout');
$blockedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('PUT', '/api/users/'.(string) $blockedUser->getId().'/block', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnblockUserWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$blockedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('PUT', '/api/users/'.(string) $blockedUser->getId().'/unblock', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanBlockUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('PUT', '/api/users/'.(string) $followedUser->getId().'/block', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('userId', $jsonData);
self::assertArrayHasKey('username', $jsonData);
self::assertArrayHasKey('about', $jsonData);
self::assertArrayHasKey('avatar', $jsonData);
self::assertArrayHasKey('cover', $jsonData);
self::assertArrayNotHasKey('lastActive', $jsonData);
self::assertArrayHasKey('createdAt', $jsonData);
self::assertArrayHasKey('followersCount', $jsonData);
self::assertArrayHasKey('apId', $jsonData);
self::assertArrayHasKey('apProfileId', $jsonData);
self::assertArrayHasKey('isBot', $jsonData);
self::assertArrayHasKey('isFollowedByUser', $jsonData);
self::assertArrayHasKey('isFollowerOfUser', $jsonData);
self::assertArrayHasKey('isBlockedByUser', $jsonData);
self::assertSame(0, $jsonData['followersCount']);
self::assertFalse($jsonData['isFollowedByUser']);
self::assertFalse($jsonData['isFollowerOfUser']);
self::assertTrue($jsonData['isBlockedByUser']);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanUnblockUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$blockedUser = $this->getUserByUsername('JohnDoe');
$testUser->block($blockedUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('PUT', '/api/users/'.(string) $blockedUser->getId().'/unblock', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('userId', $jsonData);
self::assertArrayHasKey('username', $jsonData);
self::assertArrayHasKey('about', $jsonData);
self::assertArrayHasKey('avatar', $jsonData);
self::assertArrayHasKey('cover', $jsonData);
self::assertArrayNotHasKey('lastActive', $jsonData);
self::assertArrayHasKey('createdAt', $jsonData);
self::assertArrayHasKey('followersCount', $jsonData);
self::assertArrayHasKey('apId', $jsonData);
self::assertArrayHasKey('apProfileId', $jsonData);
self::assertArrayHasKey('isBot', $jsonData);
self::assertArrayHasKey('isFollowedByUser', $jsonData);
self::assertArrayHasKey('isFollowerOfUser', $jsonData);
self::assertArrayHasKey('isBlockedByUser', $jsonData);
self::assertSame(0, $jsonData['followersCount']);
self::assertFalse($jsonData['isFollowedByUser']);
self::assertFalse($jsonData['isFollowerOfUser']);
self::assertFalse($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserContentApiTest.php
================================================
getUserByUsername('JohnDoe');
$dummyUser = $this->getUserByUsername('dummy');
$magazine = $this->getMagazineByName('test');
$entry1 = $this->createEntry('e 1', $magazine, $user);
$entry2 = $this->createEntry('e 2', $magazine, $user);
$entryDummy = $this->createEntry('dummy', $magazine, $dummyUser);
$post1 = $this->createPost('p 1', $magazine, $user);
$post2 = $this->createPost('p 2', $magazine, $user);
$this->createPost('dummy', $magazine, $dummyUser);
$comment1 = $this->createEntryComment('c 1', $entryDummy, $user);
$comment2 = $this->createEntryComment('c 2', $entryDummy, $user);
$this->createEntryComment('dummy', $entryDummy, $dummyUser);
$reply1 = $this->createPostComment('r 1', $post1, $user);
$reply2 = $this->createPostComment('r 2', $post1, $user);
$this->createPostComment('dummy', $post1, $dummyUser);
$this->client->request('GET', "/api/users/{$user->getId()}/content");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(8, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(8, $jsonData['pagination']['count']);
self::assertTrue(array_all($jsonData['items'], function ($item) use ($entry1, $entry2, $post1, $post2, $comment1, $comment2, $reply1, $reply2) {
return
(null !== $item['entry'] && ($item['entry']['entryId'] === $entry1->getId() || $item['entry']['entryId'] === $entry2->getId()))
|| (null !== $item['post'] && ($item['post']['postId'] === $post1->getId() || $item['post']['postId'] === $post2->getId()))
|| (null !== $item['entryComment'] && ($item['entryComment']['commentId'] === $comment1->getId() || $item['entryComment']['commentId'] === $comment2->getId()))
|| (null !== $item['postComment'] && ($item['postComment']['commentId'] === $reply1->getId() || $item['postComment']['commentId'] === $reply2->getId()))
;
}));
}
public function testCanGetUserContentHideAdult()
{
$user = $this->getUserByUsername('JohnDoe');
$dummyUser = $this->getUserByUsername('dummy');
$magazine = $this->getMagazineByName('test');
$entry1 = $this->createEntry('e 1', $magazine, $user);
$entry2 = $this->createEntry('e 2', $magazine, $user);
$entryDummy = $this->createEntry('dummy', $magazine, $dummyUser);
$post1 = $this->createPost('p 1', $magazine, $user);
$post2 = $this->createPost('p 2', $magazine, $user);
$this->createPost('dummy', $magazine, $dummyUser);
$comment1 = $this->createEntryComment('c 1', $entryDummy, $user);
$comment2 = $this->createEntryComment('c 2', $entryDummy, $user);
$this->createEntryComment('dummy', $entryDummy, $dummyUser);
$reply1 = $this->createPostComment('r 1', $post1, $user);
$reply2 = $this->createPostComment('r 2', $post1, $user);
$this->createPostComment('dummy', $post1, $dummyUser);
$entry2->isAdult = true;
$post2->isAdult = true;
$comment2->isAdult = true;
$reply2->isAdult = true;
$this->entityManager->persist($entry2);
$this->entityManager->persist($post2);
$this->entityManager->persist($comment2);
$this->entityManager->persist($reply2);
$this->entityManager->flush();
$this->client->request('GET', "/api/users/{$user->getId()}/content?hideAdult=true");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(4, $jsonData['pagination']['count']);
self::assertTrue(array_all($jsonData['items'], function ($item) use ($entry1, $post1, $comment1, $reply1) {
return
(null !== $item['entry'] && $item['entry']['entryId'] === $entry1->getId())
|| (null !== $item['post'] && $item['post']['postId'] === $post1->getId())
|| (null !== $item['entryComment'] && $item['entryComment']['commentId'] === $comment1->getId())
|| (null !== $item['postComment'] && $item['postComment']['commentId'] === $reply1->getId())
;
}));
}
public function testCanGetUserBoosts()
{
$user = $this->getUserByUsername('JohnDoe');
$dummyUser = $this->getUserByUsername('dummy');
$magazine = $this->getMagazineByName('test');
$entry1 = $this->createEntry('e 1', $magazine, $dummyUser);
$entry2 = $this->createEntry('e 2', $magazine, $dummyUser);
$entryDummy = $this->createEntry('dummy', $magazine, $dummyUser);
$post1 = $this->createPost('p 1', $magazine, $dummyUser);
$post2 = $this->createPost('p 2', $magazine, $dummyUser);
$this->createPost('dummy', $magazine, $dummyUser);
$comment1 = $this->createEntryComment('c 1', $entryDummy, $dummyUser);
$comment2 = $this->createEntryComment('c 2', $entryDummy, $dummyUser);
$this->createEntryComment('dummy', $entryDummy, $dummyUser);
$reply1 = $this->createPostComment('r 1', $post1, $dummyUser);
$reply2 = $this->createPostComment('r 2', $post1, $dummyUser);
$this->createPostComment('dummy', $post1, $dummyUser);
$this->voteManager->upvote($entry1, $user);
$this->voteManager->upvote($entry2, $user);
$this->voteManager->upvote($post1, $user);
$this->voteManager->upvote($post2, $user);
$this->voteManager->upvote($comment1, $user);
$this->voteManager->upvote($comment2, $user);
$this->voteManager->upvote($reply1, $user);
$this->voteManager->upvote($reply2, $user);
$this->client->request('GET', "/api/users/{$user->getId()}/boosts");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(8, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(8, $jsonData['pagination']['count']);
self::assertTrue(array_all($jsonData['items'], function ($item) use ($entry1, $entry2, $post1, $post2, $comment1, $comment2, $reply1, $reply2) {
return
(null !== $item['entry'] && ($item['entry']['entryId'] === $entry1->getId() || $item['entry']['entryId'] === $entry2->getId()))
|| (null !== $item['post'] && ($item['post']['postId'] === $post1->getId() || $item['post']['postId'] === $post2->getId()))
|| (null !== $item['entryComment'] && ($item['entryComment']['commentId'] === $comment1->getId() || $item['entryComment']['commentId'] === $comment2->getId()))
|| (null !== $item['postComment'] && ($item['postComment']['commentId'] === $reply1->getId() || $item['postComment']['commentId'] === $reply2->getId()))
;
}));
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserFilterListApiTest.php
================================================
getListUserToken();
$this->client->request('GET', '/api/users/filterLists', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertArrayHasKey('items', $data);
self::assertCount(1, $data['items']);
$list = $data['items'][0];
self::assertArrayKeysMatch(self::USER_FILTER_LIST_KEYS, $list);
}
public function testAnonymousCannotRetrieve(): void
{
$this->client->request('GET', '/api/users/filterLists');
self::assertResponseStatusCodeSame(401);
}
public function testUserCanEditList(): void
{
$token = $this->getListUserToken();
$requestParams = [
'name' => 'Some new Name',
'expirationDate' => (new \DateTimeImmutable('now - 5 days'))->format(DATE_ATOM),
'feeds' => false,
'profile' => false,
'comments' => false,
'words' => [
[
'exactMatch' => true,
'word' => 'newWord',
],
[
'exactMatch' => false,
'word' => 'sOmEnEwWoRd',
],
],
];
$this->client->jsonRequest('PUT', '/api/users/filterLists/'.$this->list->getId(), $requestParams, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertArrayIsEqualToArrayIgnoringListOfKeys($requestParams, $data, ['id']);
}
public function testOtherUserCannotEditList(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->otherUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'user:profile:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$requestParams = [
'name' => 'Some new Name',
'expirationDate' => null,
'feeds' => false,
'profile' => false,
'comments' => false,
'words' => $this->list->words,
];
$this->client->jsonRequest('PUT', '/api/users/filterLists/'.$this->list->getId(), $requestParams, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
}
public function testUserCanDeleteList(): void
{
$token = $this->getListUserToken();
$this->client->jsonRequest('DELETE', '/api/users/filterLists/'.$this->list->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$freshList = $this->entityManager->getRepository(UserFilterList::class)->find($this->list->getId());
self::assertNull($freshList);
}
public function testOtherUserCannotDeleteList(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->otherUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'user:profile:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
$this->client->jsonRequest('DELETE', '/api/users/filterLists/'.$this->list->getId(), server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseStatusCodeSame(403);
$freshList = $this->entityManager->getRepository(UserFilterList::class)->find($this->list->getId());
self::assertNotNull($freshList);
}
public function testFilteredHomePage(): void
{
$token = $this->getListUserToken();
$this->deactivateFilterList($token);
$entry = $this->getEntryByTitle('Cringe entry', body: 'some entry');
$entry2 = $this->getEntryByTitle('Some entry', body: 'some entry');
$entry2->createdAt = new \DateTimeImmutable('now - 1 minutes');
$entry3 = $this->getEntryByTitle('Some other entry', body: 'some entry with a cringe body');
$entry3->createdAt = new \DateTimeImmutable('now - 2 minutes');
$post = $this->createPost('Cringe body');
$post->createdAt = new \DateTimeImmutable('now - 3 minutes');
$post2 = $this->createPost('Body with a cringe text');
$post2->createdAt = new \DateTimeImmutable('now - 4 minutes');
$post3 = $this->createPost('Some post');
$post3->createdAt = new \DateTimeImmutable('now - 5 minutes');
$this->entityManager->flush();
$this->client->jsonRequest('GET', '/api/combined?sort=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertIsArray($data);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $data);
self::assertIsArray($data['items']);
self::assertCount(6, $data['items']);
self::assertEquals($entry->getId(), $data['items'][0]['entry']['entryId']);
self::assertEquals($entry2->getId(), $data['items'][1]['entry']['entryId']);
self::assertEquals($entry3->getId(), $data['items'][2]['entry']['entryId']);
self::assertEquals($post->getId(), $data['items'][3]['post']['postId']);
self::assertEquals($post2->getId(), $data['items'][4]['post']['postId']);
self::assertEquals($post3->getId(), $data['items'][5]['post']['postId']);
// activate list
$this->activateFilterList($token);
$this->client->jsonRequest('GET', '/api/combined?sortBy=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertIsArray($data);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $data);
self::assertIsArray($data['items']);
self::assertCount(2, $data['items']);
self::assertEquals($entry2->getId(), $data['items'][0]['entry']['entryId']);
self::assertEquals($post3->getId(), $data['items'][1]['post']['postId']);
}
public function testFilteredHomePageExact(): void
{
$token = $this->getListUserToken();
$this->deactivateFilterList($token);
$entry = $this->getEntryByTitle('TEST entry', body: 'some entry');
$entry2 = $this->getEntryByTitle('Some entry', body: 'some test entry');
$entry2->createdAt = new \DateTimeImmutable('now - 1 minutes');
$entry3 = $this->getEntryByTitle('Some other entry', body: 'some entry with a TEST body');
$entry3->createdAt = new \DateTimeImmutable('now - 2 minutes');
$post = $this->createPost('TEST body');
$post->createdAt = new \DateTimeImmutable('now - 3 minutes');
$post2 = $this->createPost('Body with a TEST text');
$post2->createdAt = new \DateTimeImmutable('now - 4 minutes');
$post3 = $this->createPost('Some test post');
$post3->createdAt = new \DateTimeImmutable('now - 5 minutes');
$this->entityManager->flush();
$this->client->jsonRequest('GET', '/api/combined?sort=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertIsArray($data);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $data);
self::assertIsArray($data['items']);
self::assertCount(6, $data['items']);
self::assertEquals($entry->getId(), $data['items'][0]['entry']['entryId']);
self::assertEquals($entry2->getId(), $data['items'][1]['entry']['entryId']);
self::assertEquals($entry3->getId(), $data['items'][2]['entry']['entryId']);
self::assertEquals($post->getId(), $data['items'][3]['post']['postId']);
self::assertEquals($post2->getId(), $data['items'][4]['post']['postId']);
self::assertEquals($post3->getId(), $data['items'][5]['post']['postId']);
$this->activateFilterList($token);
$this->client->jsonRequest('GET', '/api/combined?sortBy=newest', server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$data = self::getJsonResponse($this->client);
self::assertIsArray($data);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $data);
self::assertIsArray($data['items']);
self::assertCount(2, $data['items']);
self::assertEquals($entry2->getId(), $data['items'][0]['entry']['entryId']);
self::assertEquals($post3->getId(), $data['items'][1]['post']['postId']);
}
public function testFilteredEntryComments(): void
{
$token = $this->getListUserToken();
$entry = $this->getEntryByTitle('Some Entry');
$comment1 = $this->createEntryComment('Some normal comment', $entry);
$comment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment1 = $this->createEntryComment('Some sub comment', $entry, parent: $comment1);
$subComment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment2 = $this->createEntryComment('Some Cringe sub comment', $entry, parent: $comment1);
$subComment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$subComment3 = $this->createEntryComment('Some other Cringe sub comment', $entry, parent: $comment1);
$subComment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$comment2 = $this->createEntryComment('Some cringe comment', $entry);
$comment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$comment3 = $this->createEntryComment('Some other Cringe comment', $entry);
$comment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertEquals($comment2->getId(), $jsonData['items'][1]['commentId']);
self::assertEquals($comment3->getId(), $jsonData['items'][2]['commentId']);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment2->getId(), $jsonData['items'][0]['children'][1]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][2]['commentId']);
$this->activateFilterList($token);
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertCount(1, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
}
public function testFilteredEntryCommentsExact(): void
{
$token = $this->getListUserToken();
$entry = $this->getEntryByTitle('Some Entry');
$comment1 = $this->createEntryComment('Some normal comment', $entry);
$comment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment1 = $this->createEntryComment('Some sub comment', $entry, parent: $comment1);
$subComment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment2 = $this->createEntryComment('Some TEST sub comment', $entry, parent: $comment1);
$subComment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$subComment3 = $this->createEntryComment('Some other test sub comment', $entry, parent: $comment1);
$subComment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$comment2 = $this->createEntryComment('Some TEST comment', $entry);
$comment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$comment3 = $this->createEntryComment('Some other test comment', $entry);
$comment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertEquals($comment2->getId(), $jsonData['items'][1]['commentId']);
self::assertEquals($comment3->getId(), $jsonData['items'][2]['commentId']);
self::assertCount(3, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment2->getId(), $jsonData['items'][0]['children'][1]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][2]['commentId']);
$this->activateFilterList($token);
$this->client->request('GET', "/api/entry/{$entry->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertCount(2, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][1]['commentId']);
}
public function testFilteredPostComments(): void
{
$token = $this->getListUserToken();
$post = $this->createPost('Some Post');
$comment1 = $this->createPostComment('Some normal comment', $post);
$comment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment1 = $this->createPostComment('Some sub comment', $post, parent: $comment1);
$subComment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment2 = $this->createPostComment('Some Cringe sub comment', $post, parent: $comment1);
$subComment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$subComment3 = $this->createPostComment('Some other Cringe sub comment', $post, parent: $comment1);
$subComment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$comment2 = $this->createPostComment('Some cringe comment', $post);
$comment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$comment3 = $this->createPostComment('Some other Cringe comment', $post);
$comment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertEquals($comment2->getId(), $jsonData['items'][1]['commentId']);
self::assertEquals($comment3->getId(), $jsonData['items'][2]['commentId']);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment2->getId(), $jsonData['items'][0]['children'][1]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][2]['commentId']);
$this->activateFilterList($token);
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(1, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertCount(1, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
}
public function testFilteredPostCommentsExact(): void
{
$token = $this->getListUserToken();
$post = $this->createPost('Some Post');
$comment1 = $this->createPostComment('Some normal comment', $post);
$comment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment1 = $this->createPostComment('Some sub comment', $post, parent: $comment1);
$subComment1->createdAt = new \DateTimeImmutable('now - 1 minutes');
$subComment2 = $this->createPostComment('Some TEST sub comment', $post, parent: $comment1);
$subComment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$subComment3 = $this->createPostComment('Some other test sub comment', $post, parent: $comment1);
$subComment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$comment2 = $this->createPostComment('Some TEST comment', $post);
$comment2->createdAt = new \DateTimeImmutable('now - 2 minutes');
$comment3 = $this->createPostComment('Some other test comment', $post);
$comment3->createdAt = new \DateTimeImmutable('now - 3 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(3, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertEquals($comment2->getId(), $jsonData['items'][1]['commentId']);
self::assertEquals($comment3->getId(), $jsonData['items'][2]['commentId']);
self::assertCount(3, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment2->getId(), $jsonData['items'][0]['children'][1]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][2]['commentId']);
$this->activateFilterList($token);
$this->client->request('GET', "/api/posts/{$post->getId()}/comments?sortBy=newest", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertEquals($comment1->getId(), $jsonData['items'][0]['commentId']);
self::assertCount(2, $jsonData['items'][0]['children']);
self::assertEquals($subComment1->getId(), $jsonData['items'][0]['children'][0]['commentId']);
self::assertEquals($subComment3->getId(), $jsonData['items'][0]['children'][1]['commentId']);
}
public function testFilteredProfile(): void
{
$token = $this->getListUserToken();
$otherUser = $this->userRepository->findOneByUsername('otherUser');
$magazine = $this->getMagazineByName('someMag');
$entry = $this->createEntry('Some Entry', $magazine, $otherUser);
$entry->createdAt = new \DateTimeImmutable('now - 10 minutes');
$entryComment1 = $this->createEntryComment('Some comment', $entry, user: $otherUser);
$entryComment1->createdAt = new \DateTimeImmutable('now - 9 minutes');
$entryComment2 = $this->createEntryComment('Some cringe comment', $entry, user: $otherUser);
$entryComment2->createdAt = new \DateTimeImmutable('now - 8 minutes');
$entryComment3 = $this->createEntryComment('Some Cringe comment', $entry, user: $otherUser);
$entryComment3->createdAt = new \DateTimeImmutable('now - 7 minutes');
$entry2 = $this->getEntryByTitle('Some cringe Entry', user: $otherUser);
$entry2->createdAt = new \DateTimeImmutable('now - 6 minutes');
$post = $this->createPost('Some Post', user: $otherUser);
$post->createdAt = new \DateTimeImmutable('now - 5 minutes');
$postComment1 = $this->createPostComment('Some comment', $post, user: $otherUser);
$postComment1->createdAt = new \DateTimeImmutable('now - 4 minutes');
$postComment2 = $this->createPostComment('Some cringe comment', $post, user: $otherUser);
$postComment2->createdAt = new \DateTimeImmutable('now - 3 minutes');
$postComment3 = $this->createPostComment('Some Cringe comment', $post, user: $otherUser);
$postComment3->createdAt = new \DateTimeImmutable('now - 2 minutes');
$post2 = $this->createPost('Some Cringe Post', user: $otherUser);
$post2->createdAt = new \DateTimeImmutable('now - 1 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->jsonRequest('GET', "/api/users/{$otherUser->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertEquals($entry->getId(), $jsonData['items'][9]['entry']['entryId']);
self::assertEquals($entryComment1->getId(), $jsonData['items'][8]['entryComment']['commentId']);
self::assertEquals($entryComment2->getId(), $jsonData['items'][7]['entryComment']['commentId']);
self::assertEquals($entryComment3->getId(), $jsonData['items'][6]['entryComment']['commentId']);
self::assertEquals($entry2->getId(), $jsonData['items'][5]['entry']['entryId']);
self::assertEquals($post->getId(), $jsonData['items'][4]['post']['postId']);
self::assertEquals($postComment1->getId(), $jsonData['items'][3]['postComment']['commentId']);
self::assertEquals($postComment2->getId(), $jsonData['items'][2]['postComment']['commentId']);
self::assertEquals($postComment3->getId(), $jsonData['items'][1]['postComment']['commentId']);
self::assertEquals($post2->getId(), $jsonData['items'][0]['post']['postId']);
$this->activateFilterList($token);
$this->client->jsonRequest('GET', "/api/users/{$otherUser->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(4, $jsonData['items']);
self::assertEquals($entry->getId(), $jsonData['items'][3]['entry']['entryId']);
self::assertEquals($entryComment1->getId(), $jsonData['items'][2]['entryComment']['commentId']);
self::assertEquals($post->getId(), $jsonData['items'][1]['post']['postId']);
self::assertEquals($postComment1->getId(), $jsonData['items'][0]['postComment']['commentId']);
}
public function testFilteredProfileExact(): void
{
$token = $this->getListUserToken();
$otherUser = $this->userRepository->findOneByUsername('otherUser');
$magazine = $this->getMagazineByName('someMag');
$entry = $this->createEntry('Some Entry', $magazine, $otherUser);
$entry->createdAt = new \DateTimeImmutable('now - 10 minutes');
$entryComment1 = $this->createEntryComment('Some comment', $entry, user: $otherUser);
$entryComment1->createdAt = new \DateTimeImmutable('now - 9 minutes');
$entryComment2 = $this->createEntryComment('Some TEST comment', $entry, user: $otherUser);
$entryComment2->createdAt = new \DateTimeImmutable('now - 8 minutes');
$entryComment3 = $this->createEntryComment('Some test comment', $entry, user: $otherUser);
$entryComment3->createdAt = new \DateTimeImmutable('now - 7 minutes');
$entry2 = $this->getEntryByTitle('Some TEST Entry', user: $otherUser);
$entry2->createdAt = new \DateTimeImmutable('now - 6 minutes');
$post = $this->createPost('Some Post', user: $otherUser);
$post->createdAt = new \DateTimeImmutable('now - 5 minutes');
$postComment1 = $this->createPostComment('Some comment', $post, user: $otherUser);
$postComment1->createdAt = new \DateTimeImmutable('now - 4 minutes');
$postComment2 = $this->createPostComment('Some TEST comment', $post, user: $otherUser);
$postComment2->createdAt = new \DateTimeImmutable('now - 3 minutes');
$postComment3 = $this->createPostComment('Some test comment', $post, user: $otherUser);
$postComment3->createdAt = new \DateTimeImmutable('now - 2 minutes');
$post2 = $this->createPost('Some TEST Post', user: $otherUser);
$post2->createdAt = new \DateTimeImmutable('now - 1 minutes');
$this->entityManager->flush();
$this->deactivateFilterList($token);
$this->client->jsonRequest('GET', "/api/users/{$otherUser->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(10, $jsonData['items']);
self::assertEquals($entry->getId(), $jsonData['items'][9]['entry']['entryId']);
self::assertEquals($entryComment1->getId(), $jsonData['items'][8]['entryComment']['commentId']);
self::assertEquals($entryComment2->getId(), $jsonData['items'][7]['entryComment']['commentId']);
self::assertEquals($entryComment3->getId(), $jsonData['items'][6]['entryComment']['commentId']);
self::assertEquals($entry2->getId(), $jsonData['items'][5]['entry']['entryId']);
self::assertEquals($post->getId(), $jsonData['items'][4]['post']['postId']);
self::assertEquals($postComment1->getId(), $jsonData['items'][3]['postComment']['commentId']);
self::assertEquals($postComment2->getId(), $jsonData['items'][2]['postComment']['commentId']);
self::assertEquals($postComment3->getId(), $jsonData['items'][1]['postComment']['commentId']);
self::assertEquals($post2->getId(), $jsonData['items'][0]['post']['postId']);
$this->activateFilterList($token);
$this->client->jsonRequest('GET', "/api/users/{$otherUser->getId()}/content", server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['items']);
self::assertCount(6, $jsonData['items']);
self::assertEquals($entry->getId(), $jsonData['items'][5]['entry']['entryId']);
self::assertEquals($entryComment1->getId(), $jsonData['items'][4]['entryComment']['commentId']);
self::assertEquals($entryComment3->getId(), $jsonData['items'][3]['entryComment']['commentId']);
self::assertEquals($post->getId(), $jsonData['items'][2]['post']['postId']);
self::assertEquals($postComment1->getId(), $jsonData['items'][1]['postComment']['commentId']);
self::assertEquals($postComment3->getId(), $jsonData['items'][0]['postComment']['commentId']);
}
public function setUp(): void
{
parent::setUp();
$this->listUser = $this->getUserByUsername('listOwner');
$this->otherUser = $this->getUserByUsername('otherUser');
$this->list = new UserFilterList();
$this->list->name = 'Test List';
$this->list->user = $this->listUser;
$this->list->expirationDate = null;
$this->list->feeds = true;
$this->list->profile = true;
$this->list->comments = true;
$this->list->words = [
[
'exactMatch' => true,
'word' => 'TEST',
],
[
'exactMatch' => false,
'word' => 'Cringe',
],
];
$this->entityManager->persist($this->list);
$this->entityManager->flush();
}
private function deactivateFilterList(string $token): void
{
$dto = $this->getFilterListDto();
$this->client->jsonRequest('PUT', '/api/users/filterLists/'.$this->list->getId(), $dto, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
}
private function activateFilterList(string $token): void
{
$dto = $this->getFilterListDto();
$dto['expirationDate'] = null;
$this->client->jsonRequest('PUT', '/api/users/filterLists/'.$this->list->getId(), $dto, server: ['HTTP_AUTHORIZATION' => $token]);
self::assertResponseIsSuccessful();
}
private function getFilterListDto(): array
{
return [
'name' => $this->list->name,
'expirationDate' => (new \DateTimeImmutable('now - 1 day'))->format(DATE_ATOM),
'feeds' => true,
'profile' => true,
'comments' => true,
'words' => $this->list->words,
];
}
private function getListUserToken(): string
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->listUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'user:profile:edit');
$token = $codes['token_type'].' '.$codes['access_token'];
return $token;
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserFollowApiTest.php
================================================
getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('PUT', '/api/users/'.(string) $followedUser->getId().'/follow', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUnfollowUserWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('PUT', '/api/users/'.(string) $followedUser->getId().'/unfollow', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
#[Group(name: 'NonThreadSafe')]
public function testApiCanFollowUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('PUT', '/api/users/'.(string) $followedUser->getId().'/follow', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('userId', $jsonData);
self::assertArrayHasKey('username', $jsonData);
self::assertArrayHasKey('about', $jsonData);
self::assertArrayHasKey('avatar', $jsonData);
self::assertArrayHasKey('cover', $jsonData);
self::assertArrayNotHasKey('lastActive', $jsonData);
self::assertArrayHasKey('createdAt', $jsonData);
self::assertArrayHasKey('followersCount', $jsonData);
self::assertArrayHasKey('apId', $jsonData);
self::assertArrayHasKey('apProfileId', $jsonData);
self::assertArrayHasKey('isBot', $jsonData);
self::assertArrayHasKey('isFollowedByUser', $jsonData);
self::assertArrayHasKey('isFollowerOfUser', $jsonData);
self::assertArrayHasKey('isBlockedByUser', $jsonData);
self::assertSame(1, $jsonData['followersCount']);
self::assertTrue($jsonData['isFollowedByUser']);
self::assertFalse($jsonData['isFollowerOfUser']);
self::assertFalse($jsonData['isBlockedByUser']);
}
public function testApiCanUnfollowUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$testUser->follow($followedUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('PUT', '/api/users/'.(string) $followedUser->getId().'/unfollow', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('userId', $jsonData);
self::assertArrayHasKey('username', $jsonData);
self::assertArrayHasKey('about', $jsonData);
self::assertArrayHasKey('avatar', $jsonData);
self::assertArrayHasKey('cover', $jsonData);
self::assertArrayNotHasKey('lastActive', $jsonData);
self::assertArrayHasKey('createdAt', $jsonData);
self::assertArrayHasKey('followersCount', $jsonData);
self::assertArrayHasKey('apId', $jsonData);
self::assertArrayHasKey('apProfileId', $jsonData);
self::assertArrayHasKey('isBot', $jsonData);
self::assertArrayHasKey('isFollowedByUser', $jsonData);
self::assertArrayHasKey('isFollowerOfUser', $jsonData);
self::assertArrayHasKey('isBlockedByUser', $jsonData);
self::assertSame(0, $jsonData['followersCount']);
self::assertFalse($jsonData['isFollowedByUser']);
self::assertFalse($jsonData['isFollowerOfUser']);
self::assertFalse($jsonData['isBlockedByUser']);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserModeratesApiTest.php
================================================
getUserByUsername('JohnDoe');
$user = $this->getUserByUsername('user');
$magazine1 = $this->getMagazineByName('m 1');
$magazine2 = $this->getMagazineByName('m 2');
$this->getMagazineByName('dummy');
$this->magazineManager->addModerator(new ModeratorDto($magazine1, $user, $owner));
$this->magazineManager->addModerator(new ModeratorDto($magazine2, $user, $owner));
$this->client->request('GET', "/api/users/{$user->getId()}/moderatedMagazines");
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertCount(2, $jsonData['items']);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertTrue(array_all($jsonData['items'], function ($item) use ($magazine1, $magazine2) {
return $item['magazineId'] === $magazine1->getId() || $item['magazineId'] === $magazine2->getId();
}));
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserRetrieveApiTest.php
================================================
getUserByUsername('user'.(string) ($i + 1), about: 'Test user '.(string) ($i + 1));
}
$this->getUserByUsername('userWithoutAbout');
$this->client->request('GET', '/api/users?withAbout=1');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(self::NUM_USERS, $jsonData['pagination']['count']);
self::assertSame(1, $jsonData['pagination']['currentPage']);
self::assertSame(1, $jsonData['pagination']['maxPage']);
// Default perPage count should be used since no perPage value was specified
self::assertSame(UserRepository::PER_PAGE, $jsonData['pagination']['perPage']);
self::assertIsArray($jsonData['items']);
self::assertSame(self::NUM_USERS, \count($jsonData['items']));
}
public function testApiCanRetrieveAdminsAnonymous(): void
{
$users = [];
for ($i = 0; $i < self::NUM_USERS; ++$i) {
$users[] = $this->getUserByUsername('admin'.(string) ($i + 1), isAdmin: true);
}
$this->client->request('GET', '/api/users/admins');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertIsArray($jsonData['items']);
self::assertSame(self::NUM_USERS, \count($jsonData['items']));
}
public function testApiCanRetrieveModeratorsAnonymous(): void
{
$users = [];
for ($i = 0; $i < self::NUM_USERS; ++$i) {
$users[] = $this->getUserByUsername('moderator'.(string) ($i + 1), isModerator: true);
}
$this->client->request('GET', '/api/users/moderators');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertIsArray($jsonData['items']);
self::assertSame(self::NUM_USERS, \count($jsonData['items']));
}
public function testApiCanRetrieveUsersWithAbout(): void
{
self::createOAuth2AuthCodeClient();
$this->client->loginUser($this->getUserByUsername('UserWithoutAbout'));
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$users = [];
for ($i = 0; $i < self::NUM_USERS; ++$i) {
$users[] = $this->getUserByUsername('user'.(string) ($i + 1), about: 'Test user '.(string) ($i + 1));
}
$this->client->request('GET', '/api/users?withAbout=1', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(self::NUM_USERS, $jsonData['pagination']['count']);
}
public function testApiCanRetrieveUserByIdAnonymous(): void
{
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId());
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertSame('UserWithoutAbout', $jsonData['username']);
self::assertNull($jsonData['about']);
self::assertNotNull($jsonData['createdAt']);
self::assertFalse($jsonData['isBot']);
self::assertNull($jsonData['apId']);
// Follow and block scopes not assigned, so these flags should be null
self::assertNull($jsonData['isFollowedByUser']);
self::assertNull($jsonData['isFollowerOfUser']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveUserById(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertSame('UserWithoutAbout', $jsonData['username']);
self::assertNull($jsonData['about']);
self::assertNotNull($jsonData['createdAt']);
self::assertFalse($jsonData['isBot']);
self::assertNull($jsonData['apId']);
// Follow and block scopes not assigned, so these flags should be null
self::assertNull($jsonData['isFollowedByUser']);
self::assertNull($jsonData['isFollowerOfUser']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveUserByNameAnonymous(): void
{
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->request('GET', '/api/users/name/'.$testUser->getUsername());
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertSame('UserWithoutAbout', $jsonData['username']);
self::assertNull($jsonData['about']);
self::assertNotNull($jsonData['createdAt']);
self::assertFalse($jsonData['isBot']);
self::assertNull($jsonData['apId']);
// Follow and block scopes not assigned, so these flags should be null
self::assertNull($jsonData['isFollowedByUser']);
self::assertNull($jsonData['isFollowerOfUser']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveUserByName(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/name/'.$testUser->getUsername(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertSame('UserWithoutAbout', $jsonData['username']);
self::assertNull($jsonData['about']);
self::assertNotNull($jsonData['createdAt']);
self::assertFalse($jsonData['isBot']);
self::assertNull($jsonData['apId']);
// Follow and block scopes not assigned, so these flags should be null
self::assertNull($jsonData['isFollowedByUser']);
self::assertNull($jsonData['isFollowerOfUser']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCannotRetrieveCurrentUserWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/me', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanRetrieveCurrentUser(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$this->client->request('GET', '/api/users/me', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertSame('UserWithoutAbout', $jsonData['username']);
self::assertNull($jsonData['about']);
self::assertNotNull($jsonData['createdAt']);
self::assertFalse($jsonData['isBot']);
self::assertNull($jsonData['apId']);
// Follow and block scopes not assigned, so these flags should be null
self::assertNull($jsonData['isFollowedByUser']);
self::assertNull($jsonData['isFollowerOfUser']);
self::assertNull($jsonData['isBlockedByUser']);
}
public function testApiCanRetrieveUserFlagsWithScopes(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$follower = $this->getUserByUsername('follower');
$follower->follow($testUser);
$manager = $this->entityManager;
$manager->persist($follower);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/'.(string) $follower->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
// Follow and block scopes assigned, so these flags should not be null
self::assertFalse($jsonData['isFollowedByUser']);
self::assertTrue($jsonData['isFollowerOfUser']);
self::assertFalse($jsonData['isBlockedByUser']);
}
public function testApiCanGetBlockedUsers(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$blockedUser = $this->getUserByUsername('JohnDoe');
$testUser->block($blockedUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/blocked', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertSame(1, \count($jsonData['items']));
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($blockedUser->getId(), $jsonData['items'][0]['userId']);
}
public function testApiCannotGetFollowedUsersWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/followed', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotGetFollowersWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/followers', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetFollowedUsers(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$testUser->follow($followedUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/followed', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertSame(1, \count($jsonData['items']));
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($followedUser->getId(), $jsonData['items'][0]['userId']);
}
public function testApiCanGetFollowers(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followingUser = $this->getUserByUsername('JohnDoe');
$followingUser->follow($testUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/followers', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertSame(1, \count($jsonData['items']));
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($followingUser->getId(), $jsonData['items'][0]['userId']);
}
public function testApiCannotGetFollowedUsersByIdIfNotShared(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$testUser->follow($followedUser);
$testUser->showProfileFollowings = false;
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($followedUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId().'/followed', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetFollowedUsersById(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followedUser = $this->getUserByUsername('JohnDoe');
$testUser->follow($followedUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($followedUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId().'/followed', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertSame(1, \count($jsonData['items']));
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($followedUser->getId(), $jsonData['items'][0]['userId']);
}
public function testApiCanGetFollowersById(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('UserWithoutAbout');
$followingUser = $this->getUserByUsername('JohnDoe');
$followingUser->follow($testUser);
$manager = $this->entityManager;
$manager->persist($testUser);
$manager->flush();
$this->client->loginUser($followingUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId().'/followers', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertSame(1, \count($jsonData['items']));
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertSame($followingUser->getId(), $jsonData['items'][0]['userId']);
}
public function testApiCannotGetSettingsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
$this->client->request('GET', '/api/users/settings', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetSettings(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$this->client->request('GET', '/api/users/settings', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_SETTINGS_KEYS, $jsonData);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserRetrieveOAuthConsentsApiTest.php
================================================
getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:follow user:block');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetConsents(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:follow user:block');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['pagination']);
self::assertArrayKeysMatch(self::PAGINATION_KEYS, $jsonData['pagination']);
self::assertSame(1, $jsonData['pagination']['count']);
self::assertIsArray($jsonData['items']);
self::assertSame(1, \count($jsonData['items']));
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals(
['read', 'user:oauth_clients:read', 'user:follow', 'user:block'],
$jsonData['items'][0]['scopesGranted']
);
self::assertEquals(
OAuth2ClientDto::AVAILABLE_SCOPES,
$jsonData['items'][0]['scopesAvailable']
);
self::assertEquals('/kbin Test Client', $jsonData['items'][0]['client']);
self::assertEquals('An OAuth2 client for testing purposes', $jsonData['items'][0]['description']);
self::assertNull($jsonData['items'][0]['clientLogo']);
}
public function testApiCannotGetOtherUsersConsentsById(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$testUser2 = $this->getUserByUsername('someuser2');
$this->client->loginUser($testUser);
$codes1 = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:follow user:block');
$this->client->loginUser($testUser2);
$codes2 = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:follow user:block');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes1['token_type'].' '.$codes1['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertSame(1, \count($jsonData['items']));
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
$this->client->request(
'GET', '/api/users/consents/'.(string) $jsonData['items'][0]['consentId'],
server: ['HTTP_AUTHORIZATION' => $codes2['token_type'].' '.$codes2['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanGetConsentsById(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:follow user:block');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertIsArray($jsonData['items']);
self::assertSame(1, \count($jsonData['items']));
self::assertIsArray($jsonData['items'][0]);
self::assertArrayKeysMatch(self::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
$consent = $jsonData['items'][0];
$this->client->request(
'GET', '/api/users/consents/'.(string) $consent['consentId'],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertEquals($consent, $jsonData);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserUpdateApiTest.php
================================================
getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'about' => 'Updated during test',
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateCurrentUserProfile(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
$this->client->request('GET', '/api/users/'.(string) $testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertNull($jsonData['about']);
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'about' => 'Updated during test',
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertEquals('Updated during test', $jsonData['about']);
$this->client->request('GET', '/api/users/'.(string) $testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertEquals('Updated during test', $jsonData['about']);
}
public function testApiCanUpdateCurrentUserTitle(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
$this->client->request('GET', '/api/users/'.$testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertNull($jsonData['title']);
// region set title
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'title' => 'Custom user-name',
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertEquals('Custom user-name', $jsonData['title']);
$this->client->request('GET', '/api/users/'.$testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertEquals('Custom user-name', $jsonData['title']);
// endregion
// region reset title
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertNull($jsonData['title']);
$this->client->request('GET', '/api/users/'.$testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertSame($testUser->getId(), $jsonData['userId']);
self::assertNull($jsonData['title']);
// endregion
}
public function testApiCannotUpdateCurrentUserTitleWithWhitespaces()
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
$this->client->request('GET', '/api/users/'.$testUser->getId(), server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'title' => " \t",
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'title' => '',
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(400);
$this->client->jsonRequest(
'PUT', '/api/users/profile',
parameters: [
'title' => ' . ',
],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(400);
}
public function testApiCannotUpdateCurrentUserSettingsWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$settings = (new UserSettingsDto(
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
User::HOMEPAGE_MOD,
Criteria::SORT_HOT,
Criteria::SORT_HOT,
false,
['test'],
['en'],
directMessageSetting: EDirectMessageSettings::Everyone->value,
frontDefaultContent: EFrontContentOptions::Combined->value,
))->jsonSerialize();
$this->client->jsonRequest(
'PUT', '/api/users/settings',
parameters: $settings,
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateCurrentUserSettings(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
$settings = (new UserSettingsDto(
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
User::HOMEPAGE_MOD,
Criteria::SORT_NEW,
Criteria::SORT_TOP,
false,
['test'],
['en'],
directMessageSetting: EDirectMessageSettings::FollowersOnly->value,
frontDefaultContent: EFrontContentOptions::Threads->value,
discoverable: false,
indexable: false,
))->jsonSerialize();
$this->client->jsonRequest(
'PUT', '/api/users/settings',
parameters: $settings,
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(UserRetrieveApiTest::USER_SETTINGS_KEYS, $jsonData);
self::assertFalse($jsonData['notifyOnNewEntry']);
self::assertFalse($jsonData['notifyOnNewEntryReply']);
self::assertFalse($jsonData['notifyOnNewEntryCommentReply']);
self::assertFalse($jsonData['notifyOnNewPost']);
self::assertFalse($jsonData['notifyOnNewPostReply']);
self::assertFalse($jsonData['notifyOnNewPostCommentReply']);
self::assertFalse($jsonData['hideAdult']);
self::assertFalse($jsonData['showProfileSubscriptions']);
self::assertFalse($jsonData['showProfileFollowings']);
self::assertFalse($jsonData['addMentionsEntries']);
self::assertFalse($jsonData['addMentionsPosts']);
self::assertFalse($jsonData['discoverable']);
self::assertEquals(User::HOMEPAGE_MOD, $jsonData['homepage']);
self::assertEquals(Criteria::SORT_NEW, $jsonData['frontDefaultSort']);
self::assertEquals(Criteria::SORT_TOP, $jsonData['commentDefaultSort']);
self::assertEquals(['test'], $jsonData['featuredMagazines']);
self::assertEquals(['en'], $jsonData['preferredLanguages']);
self::assertEquals(EDirectMessageSettings::FollowersOnly->value, $jsonData['directMessageSetting']);
self::assertEquals(EFrontContentOptions::Threads->value, $jsonData['frontDefaultContent']);
self::assertFalse($jsonData['indexable']);
$this->client->request('GET', '/api/users/settings', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(UserRetrieveApiTest::USER_SETTINGS_KEYS, $jsonData);
self::assertFalse($jsonData['notifyOnNewEntry']);
self::assertFalse($jsonData['notifyOnNewEntryReply']);
self::assertFalse($jsonData['notifyOnNewEntryCommentReply']);
self::assertFalse($jsonData['notifyOnNewPost']);
self::assertFalse($jsonData['notifyOnNewPostReply']);
self::assertFalse($jsonData['notifyOnNewPostCommentReply']);
self::assertFalse($jsonData['hideAdult']);
self::assertFalse($jsonData['showProfileSubscriptions']);
self::assertFalse($jsonData['showProfileFollowings']);
self::assertFalse($jsonData['addMentionsEntries']);
self::assertFalse($jsonData['addMentionsPosts']);
self::assertFalse($jsonData['discoverable']);
self::assertEquals(User::HOMEPAGE_MOD, $jsonData['homepage']);
self::assertEquals(Criteria::SORT_NEW, $jsonData['frontDefaultSort']);
self::assertEquals(Criteria::SORT_TOP, $jsonData['commentDefaultSort']);
self::assertEquals(['test'], $jsonData['featuredMagazines']);
self::assertEquals(['en'], $jsonData['preferredLanguages']);
self::assertEquals(EDirectMessageSettings::FollowersOnly->value, $jsonData['directMessageSetting']);
self::assertEquals(EFrontContentOptions::Threads->value, $jsonData['frontDefaultContent']);
self::assertFalse($jsonData['indexable']);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserUpdateImagesApiTest.php
================================================
kibbyPath = \dirname(__FILE__, 5).'/assets/kibby_emoji.png';
}
public function testApiCannotUpdateCurrentUserAvatarWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', '/api/users/avatar',
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotUpdateCurrentUserCoverWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
// Uploading a file appears to delete the file at the given path, so make a copy before upload
copy($this->kibbyPath, $this->kibbyPath.'.tmp');
$image = new UploadedFile($this->kibbyPath.'.tmp', 'kibby_emoji.png', 'image/png');
$this->client->request(
'POST', '/api/users/cover',
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteCurrentUserAvatarWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$this->client->request('DELETE', '/api/users/avatar', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCannotDeleteCurrentUserCoverWithoutScope(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:read');
$this->client->request('DELETE', '/api/users/cover', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateAndDeleteCurrentUserAvatar(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$imageManager = $this->imageManager;
$expectedPath = $imageManager->getFilePath($image->getFilename());
$this->client->request(
'POST', '/api/users/avatar',
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['avatar']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['avatar']);
self::assertSame(96, $jsonData['avatar']['width']);
self::assertSame(96, $jsonData['avatar']['height']);
self::assertEquals($expectedPath, $jsonData['avatar']['filePath']);
// Clean up test data as well as checking that DELETE works
// This isn't great, but since people could have their media directory
// pretty much anywhere, its difficult to reliably clean up uploaded files
// otherwise. This is certainly something that could be improved.
$this->client->request('DELETE', '/api/users/avatar', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertNull($jsonData['avatar']);
}
public function testApiCanUpdateAndDeleteCurrentUserCover(): void
{
$imageManager = $this->imageManager;
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:profile:edit user:profile:read');
// Uploading a file appears to delete the file at the given path, so make a copy before upload
$tmpPath = bin2hex(random_bytes(32));
copy($this->kibbyPath, $tmpPath.'.png');
$image = new UploadedFile($tmpPath.'.png', 'kibby_emoji.png', 'image/png');
$expectedPath = $imageManager->getFilePath($image->getFilename());
$this->client->request(
'POST', '/api/users/cover',
files: ['uploadImage' => $image],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertIsArray($jsonData['cover']);
self::assertArrayKeysMatch(self::IMAGE_KEYS, $jsonData['cover']);
self::assertSame(96, $jsonData['cover']['width']);
self::assertSame(96, $jsonData['cover']['height']);
self::assertEquals($expectedPath, $jsonData['cover']['filePath']);
// Clean up test data as well as checking that DELETE works
// This isn't great, but since people could have their media directory
// pretty much anywhere, its difficult to reliably clean up uploaded files
// otherwise. This is certainly something that could be improved.
$this->client->request('DELETE', '/api/users/cover', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::USER_RESPONSE_KEYS, $jsonData);
self::assertNull($jsonData['cover']);
}
}
================================================
FILE: tests/Functional/Controller/Api/User/UserUpdateOAuthConsentsApiTest.php
================================================
getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(UserRetrieveOAuthConsentsApiTest::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
$this->client->jsonRequest(
'PUT', '/api/users/consents/'.(string) $jsonData['items'][0]['consentId'],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
public function testApiCanUpdateConsents(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:oauth_clients:edit user:follow');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(UserRetrieveOAuthConsentsApiTest::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals([
'read',
'user:oauth_clients:read',
'user:oauth_clients:edit',
'user:follow',
], $jsonData['items'][0]['scopesGranted']);
$this->client->jsonRequest(
'PUT', '/api/users/consents/'.(string) $jsonData['items'][0]['consentId'],
parameters: ['scopes' => [
'read',
'user:oauth_clients:read',
'user:oauth_clients:edit',
]],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(UserRetrieveOAuthConsentsApiTest::CONSENT_RESPONSE_KEYS, $jsonData);
self::assertEquals([
'read',
'user:oauth_clients:read',
'user:oauth_clients:edit',
], $jsonData['scopesGranted']);
}
public function testApiUpdatingConsentsDoesNotAffectExistingKeys(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:oauth_clients:edit user:follow');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
$this->client->jsonRequest(
'PUT', '/api/users/consents/'.(string) $jsonData['items'][0]['consentId'],
parameters: ['scopes' => [
'read',
'user:oauth_clients:edit',
]],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
// Existing token still has permission to read oauth consents despite client consent being revoked.
$this->client->jsonRequest(
'GET', '/api/users/consents/'.(string) $jsonData['consentId'],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(UserRetrieveOAuthConsentsApiTest::CONSENT_RESPONSE_KEYS, $jsonData);
self::assertEquals([
'read',
'user:oauth_clients:edit',
], $jsonData['scopesGranted']);
}
public function testApiCannotAddConsents(): void
{
self::createOAuth2AuthCodeClient();
$testUser = $this->getUserByUsername('someuser');
$this->client->loginUser($testUser);
$codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read user:oauth_clients:read user:oauth_clients:edit user:follow');
$this->client->request('GET', '/api/users/consents', server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]);
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayKeysMatch(self::PAGINATED_KEYS, $jsonData);
self::assertCount(1, $jsonData['items']);
self::assertArrayKeysMatch(UserRetrieveOAuthConsentsApiTest::CONSENT_RESPONSE_KEYS, $jsonData['items'][0]);
self::assertEquals([
'read',
'user:oauth_clients:read',
'user:oauth_clients:edit',
'user:follow',
], $jsonData['items'][0]['scopesGranted']);
$this->client->jsonRequest(
'PUT', '/api/users/consents/'.(string) $jsonData['items'][0]['consentId'],
parameters: ['scopes' => [
'read',
'user:oauth_clients:read',
'user:oauth_clients:edit',
'user:block',
]],
server: ['HTTP_AUTHORIZATION' => $codes['token_type'].' '.$codes['access_token']]
);
self::assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Domain/DomainBlockControllerTest.php
================================================
createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
// Block
$this->client->submit($crawler->filter('#sidebar form[name=domain_block] button')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=domain_block] .active');
// Unblock
$this->client->submit($crawler->filter('#sidebar form[name=domain_block] button')->form());
$this->client->followRedirect();
$this->assertSelectorNotExists('#sidebar form[name=domain_block] .active');
}
#[Group(name: 'NonThreadSafe')]
public function testXmlUserCanBlockDomain(): void
{
$entry = $this->createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=domain_block] button')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('active', $this->client->getResponse()->getContent());
}
#[Group(name: 'NonThreadSafe')]
public function testXmlUserCanUnblockDomain(): void
{
$entry = $this->createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
// Block
$this->client->submit($crawler->filter('#sidebar form[name=domain_block] button')->form());
$crawler = $this->client->followRedirect();
// Unblock
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=domain_block] button')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringNotContainsString('active', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/Domain/DomainCommentFrontControllerTest.php
================================================
createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->createEntryComment('test comment 1', $entry);
$crawler = $this->client->request('GET', '/d/kbin.pub');
$crawler = $this->client->click($crawler->filter('#header')->selectLink('Comments')->link());
$this->assertSelectorTextContains('#header', '/d/kbin.pub');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextContains('blockquote header', 'to acme in test entry 1');
$this->assertSelectorTextContains('blockquote .content', 'test comment 1');
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', 'kbin.pub');
$this->assertSelectorTextContains('h2', ucfirst($sortOption));
}
}
private function getSortOptions(): array
{
return ['Hot', 'Newest', 'Active', 'Oldest'];
}
}
================================================
FILE: tests/Functional/Controller/Domain/DomainFrontControllerTest.php
================================================
createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$crawler = $this->client->request('GET', '/');
$crawler = $this->client->click($crawler->filter('#content article')->selectLink('More from domain')->link());
$this->assertSelectorTextContains('#header', '/d/kbin.pub');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextContains('.entry__meta', 'to acme');
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', 'kbin.pub');
$this->assertSelectorTextContains('h2', ucfirst($sortOption));
}
}
private function getSortOptions(): array
{
return ['Top', 'Hot', 'Newest', 'Active', 'Commented'];
}
}
================================================
FILE: tests/Functional/Controller/Domain/DomainSubControllerTest.php
================================================
createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
// Subscribe
$this->client->submit($crawler->filter('#sidebar .domain')->selectButton('Subscribe')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=domain_subscribe] .active');
$this->assertSelectorTextContains('#sidebar .domain', 'Unsubscribe');
$this->assertSelectorTextContains('#sidebar .domain', '1');
// Unsubscribe
$this->client->submit($crawler->filter('#sidebar .domain')->selectButton('Unsubscribe')->form());
$this->client->followRedirect();
$this->assertSelectorNotExists('#sidebar form[name=domain_subscribe] .active');
$this->assertSelectorTextContains('#sidebar .domain', 'Subscribe');
$this->assertSelectorTextContains('#sidebar .domain', '0');
}
public function testXmlUserCanSubDomain(): void
{
$this->createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
// Subscribe
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .domain')->selectButton('Subscribe')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Unsubscribe', $this->client->getResponse()->getContent());
}
public function testXmlUserCanUnsubDomain(): void
{
$this->createEntry(
'test entry 1',
$this->getMagazineByName('acme'),
$this->getUserByUsername('JohnDoe'),
'http://kbin.pub/instances'
);
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', '/d/kbin.pub');
// Subscribe
$this->client->submit($crawler->filter('#sidebar .domain')->selectButton('Subscribe')->form());
$crawler = $this->client->followRedirect();
// Unsubscribe
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .domain')->selectButton('Unsubscribe')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Subscribe', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentBoostControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
null,
$this->getUserByUsername('JaneDoe')
);
$this->createEntryComment('test comment 1', $entry, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->client->submit(
$crawler->filter('#main .entry-comment')->selectButton('Boost')->form()
);
$this->client->followRedirect();
self::assertResponseIsSuccessful();
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->assertSelectorTextContains('#main .entry-comment', 'Boost (1)');
$crawler = $this->client->click($crawler->filter('#main .entry-comment')->selectLink('Activity')->link());
$this->client->click($crawler->filter('#main #activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JohnDoe');
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentChangeLangControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createEntryComment('test comment 1');
$crawler = $this->client->request('GET', "/m/acme/t/{$comment->entry->getId()}/-/comment/{$comment->getId()}/moderate");
$form = $crawler->filter('.moderate-panel')->selectButton('lang[submit]')->form();
$this->assertSame($form['lang']['lang']->getValue(), 'en');
$form['lang']['lang']->select('fr');
$this->client->submit($form);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .badge-lang', 'French');
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentCreateControllerTest.php
================================================
kibbyPath = \dirname(__FILE__, 5).'/assets/kibby_emoji.png';
}
public function testUserCanCreateEntryComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->client->submit(
$crawler->filter('form[name=entry_comment]')->selectButton('Add comment')->form(
[
'entry_comment[body]' => 'test comment 1',
]
)
);
$this->assertResponseRedirects('/m/acme/t/'.$entry->getId().'/test-entry-1');
$this->client->followRedirect();
$this->assertSelectorTextContains('#main blockquote', 'test comment 1');
}
public function testUserCannotCreateEntryCommentInLockedEntry(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->entryManager->toggleLock($entry, $user);
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
self::assertSelectorTextNotContains('#main', 'Add comment');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanCreateEntryCommentWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$form = $crawler->filter('form[name=entry_comment]')->selectButton('entry_comment[submit]')->form();
$form->get('entry_comment[body]')->setValue('test comment 1');
$form->get('entry_comment[image]')->upload($this->kibbyPath);
// Needed since we require this global to be set when validating entries but the client doesn't actually set it
$_FILES = $form->getPhpFiles();
$this->client->submit($form);
$this->assertResponseRedirects('/m/acme/t/'.$entry->getId().'/test-entry-1');
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main blockquote', 'test comment 1');
$this->assertSelectorExists('blockquote footer figure img');
$imgSrc = $crawler->filter('blockquote footer figure img')->getNode(0)->attributes->getNamedItem('src')->textContent;
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $imgSrc);
$_FILES = [];
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanReplyEntryComment(): void
{
$comment = $this->createEntryComment(
'test comment 1',
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub'),
$this->getUserByUsername('JaneDoe')
);
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#entry-comment-'.$comment->getId())->selectLink('Reply')->link());
$this->assertSelectorTextContains('#main blockquote', 'test comment 1');
$crawler = $this->client->submit(
$crawler->filter('form[name=entry_comment]')->selectButton('Add comment')->form(
[
'entry_comment[body]' => 'test comment 2',
]
)
);
$this->assertResponseRedirects('/m/acme/t/'.$entry->getId().'/test-entry-1');
$crawler = $this->client->followRedirect();
$this->assertEquals(2, $crawler->filter('#main blockquote')->count());
}
#[Group(name: 'NonThreadSafe')]
public function testUserCantCreateInvalidEntryComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->client->submit(
$crawler->filter('form[name=entry_comment]')->selectButton('Add comment')->form(
[
'entry_comment[body]' => '',
]
)
);
$this->assertSelectorTextContains(
'#content',
'This value should not be blank.'
);
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentDeleteControllerTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$entry = $this->getEntryByTitle('comment deletion test', body: 'a comment will be deleted', magazine: $magazine, user: $user);
$comment = $this->createEntryComment('Delete me!', $entry, $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/comment-deletion-test");
$this->assertSelectorExists('#comments form[action$="delete"]');
$this->client->submit(
$crawler->filter('#comments form[action$="delete"]')->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
}
public function testUserCanSoftDeleteEntryComment()
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$entry = $this->getEntryByTitle('comment deletion test', body: 'a comment will be deleted', magazine: $magazine, user: $user);
$comment = $this->createEntryComment('Delete me!', $entry, $user);
$reply = $this->createEntryComment('Are you deleted yet?', $entry, $user, $comment);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/comment-deletion-test");
$this->assertSelectorExists("#entry-comment-{$comment->getId()} form[action$=\"delete\"]");
$this->client->submit(
$crawler->filter("#entry-comment-{$comment->getId()} form[action$=\"delete\"]")->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
$crawler = $this->client->followRedirect();
$translator = $this->translator;
$this->assertSelectorTextContains("#entry-comment-{$comment->getId()} .content", $translator->trans('deleted_by_author'));
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentEditControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->createEntryComment('test comment 1', $entry);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry-comment')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .entry-comment');
$this->assertSelectorTextContains('#main .entry-comment', 'test comment 1');
$this->client->submit(
$crawler->filter('form[name=entry_comment]')->selectButton('Update comment')->form(
[
'entry_comment[body]' => 'test comment 2 body',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry-comment', 'test comment 2 body');
}
#[Group(name: 'NonThreadSafe')]
public function testAuthorCanEditOwnEntryCommentWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->createEntryComment('test comment 1', $entry, imageDto: $imageDto);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry-comment')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .entry-comment');
$this->assertSelectorTextContains('#main .entry-comment', 'test comment 1');
$this->assertSelectorExists('#main .entry-comment img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
$this->client->submit(
$crawler->filter('form[name=entry_comment]')->selectButton('Update comment')->form(
[
'entry_comment[body]' => 'test comment 2 body',
]
)
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry-comment', 'test comment 2 body');
$this->assertSelectorExists('#main .entry-comment img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentFrontControllerTest.php
================================================
client = $this->prepareEntries();
$this->client->request('GET', '/comments');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/comments/newest');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextContains('blockquote header', 'to kbin in test entry 2');
$this->assertSelectorTextContains('blockquote .content', 'test comment 3');
$this->assertcount(3, $crawler->filter('.comment'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testMagazinePage(): void
{
$this->client = $this->prepareEntries();
$this->client->request('GET', '/m/acme/comments');
$this->assertSelectorTextContains('h2', 'Hot');
$crawler = $this->client->request('GET', '/m/acme/comments/newest');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextNotContains('blockquote header', 'to acme');
$this->assertSelectorTextContains('blockquote header', 'in test entry 1');
$this->assertSelectorTextContains('blockquote .content', 'test comment 2');
$this->assertSelectorTextContains('.head-title', '/m/acme');
$this->assertSelectorTextContains('#sidebar .magazine', 'acme');
$this->assertcount(2, $crawler->filter('.comment'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', 'acme');
$this->assertSelectorTextContains('h2', ucfirst($sortOption));
}
}
public function testSubPage(): void
{
$this->client = $this->prepareEntries();
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$magazineManager->subscribe($this->getMagazineByName('acme'), $this->getUserByUsername('Actor'));
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/sub/comments');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/sub/comments/newest');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextContains('blockquote header', 'to acme in test entry 1');
$this->assertSelectorTextContains('blockquote .content', 'test comment 2');
$this->assertSelectorTextContains('.head-title', '/sub');
$this->assertcount(2, $crawler->filter('.comment'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testModPage(): void
{
$this->client = $this->prepareEntries();
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$moderator = new ModeratorDto($this->getMagazineByName('acme'));
$moderator->user = $this->getUserByUsername('Actor');
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/mod/comments');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/mod/comments/newest');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextContains('blockquote header', 'to acme in test entry 1');
$this->assertSelectorTextContains('blockquote .content', 'test comment 2');
$this->assertSelectorTextContains('.head-title', '/mod');
$this->assertcount(2, $crawler->filter('.comment'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testFavPage(): void
{
$this->client = $this->prepareEntries();
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle(
$this->getUserByUsername('Actor'),
$this->createEntryComment('test comment 1', $this->getEntryByTitle('test entry 1'))
);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/fav/comments');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/fav/comments/newest');
$this->assertSelectorTextContains('blockquote header', 'JohnDoe');
$this->assertSelectorTextContains('blockquote header', 'to acme in test entry 1');
$this->assertSelectorTextContains('blockquote .content', 'test comment 1');
$this->assertcount(1, $crawler->filter('.comment'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
private function prepareEntries(): KernelBrowser
{
$this->createEntryComment(
'test comment 1',
$this->getEntryByTitle('test entry 1', 'https://kbin.pub'),
$this->getUserByUsername('JohnDoe')
);
$this->createEntryComment(
'test comment 2',
$this->getEntryByTitle('test entry 1', 'https://kbin.pub'),
$this->getUserByUsername('JohnDoe')
);
$this->createEntryComment(
'test comment 3',
$this->getEntryByTitle('test entry 2', 'https://kbin.pub', null, $this->getMagazineByName('kbin')),
$this->getUserByUsername('JohnDoe')
);
return $this->client;
}
private function getSortOptions(): array
{
return ['Hot', 'Newest', 'Active', 'Oldest'];
}
}
================================================
FILE: tests/Functional/Controller/Entry/Comment/EntryCommentModerateControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createEntryComment('test comment 1');
$crawler = $this->client->request('get', "/m/{$comment->magazine->name}/t/{$comment->entry->getId()}");
$this->client->click($crawler->filter('#entry-comment-'.$comment->getId())->selectLink('Moderate')->link());
$this->assertSelectorTextContains('.moderate-panel', 'Ban');
}
public function testXmlModCanShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createEntryComment('test comment 1');
$crawler = $this->client->request('get', "/m/{$comment->magazine->name}/t/{$comment->entry->getId()}");
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('#entry-comment-'.$comment->getId())->selectLink('Moderate')->link());
$this->assertStringContainsString('moderate-panel', $this->client->getResponse()->getContent());
}
public function testUnauthorizedCanNotShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$comment = $this->createEntryComment('test comment 1');
$this->client->request('get', "/m/{$comment->magazine->name}/t/{$comment->entry->getId()}");
$this->assertSelectorTextNotContains('#entry-comment-'.$comment->getId(), 'Moderate');
$this->client->request(
'get',
"/m/{$comment->magazine->name}/t/{$comment->entry->getId()}/-/comment/{$comment->getId()}/moderate"
);
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryBoostControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->client->submit(
$crawler->filter('#main .entry')->selectButton('Boost')->form([])
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry', 'Boost (1)');
$this->client->click($crawler->filter('#activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JohnDoe');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryChangeAdultControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Mark NSFW')->form([
'adult' => 'on',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry .badge', '18+');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Unmark NSFW')->form([
'adult' => 'off',
])
);
$this->client->followRedirect();
$this->assertSelectorTextNotContains('#main .entry', '18+');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryChangeLangControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$form = $crawler->filter('.moderate-panel')->selectButton('Change language')->form();
$this->assertSame($form['lang']['lang']->getValue(), 'en');
$form['lang']['lang']->select('fr');
$this->client->submit($form);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .badge-lang', 'French');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryChangeMagazineControllerTest.php
================================================
getUserByUsername('JohnDoe');
$this->setAdmin($user);
$this->client->loginUser($user);
$this->getMagazineByName('kbin');
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->client->submit(
$crawler->filter('form[name=change_magazine]')->selectButton('Change magazine')->form(
[
'change_magazine[new_magazine]' => 'kbin',
]
)
);
$this->client->followRedirect();
$this->client->followRedirect();
$this->assertSelectorTextContains('.head-title', 'kbin');
}
public function testUnauthorizedUserCantChangeMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('kbin');
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
);
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->assertSelectorTextNotContains('.moderate-panel', 'Change magazine');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryCreateControllerTest.php
================================================
kibbyPath = \dirname(__FILE__, 4).'/assets/kibby_emoji.png';
}
public function testUserCanCreateEntry()
{
$this->client->loginUser($this->getUserByUsername('user'));
$this->client->request('GET', '/m/acme/new_entry');
$this->assertSelectorExists('form[name=entry]');
}
public function testUserCanCreateEntryLinkFromMagazinePage(): void
{
$this->client->loginUser($this->getUserByUsername('user'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/new_entry');
$this->client->submit(
$crawler->filter('form[name=entry]')->selectButton('Add new thread')->form(
[
'entry[url]' => 'https://kbin.pub',
'entry[title]' => 'Test entry 1',
'entry[body]' => 'Test body',
]
)
);
$this->assertResponseRedirects('/m/acme/default/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('article h2', 'Test entry 1');
}
public function testUserCanCreateEntryArticleFromMagazinePage()
{
$this->client->loginUser($this->getUserByUsername('user'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/new_entry');
$this->client->submit(
$crawler->filter('form[name=entry]')->selectButton('Add new thread')->form(
[
'entry[title]' => 'Test entry 1',
'entry[body]' => 'Test body',
]
)
);
$this->assertResponseRedirects('/m/acme/default/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('article h2', 'Test entry 1');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanCreateEntryPhotoFromMagazinePage()
{
$this->client->loginUser($this->getUserByUsername('user'));
$this->getMagazineByName('acme');
$repository = $this->entryRepository;
$crawler = $this->client->request('GET', '/m/acme/new_entry');
$this->assertSelectorExists('form[name=entry]');
$form = $crawler->filter('#main form[name=entry]')->selectButton('Add new thread')->form([
'entry[title]' => 'Test image 1',
'entry[image]' => $this->kibbyPath,
]);
// Needed since we require this global to be set when validating entries but the client doesn't actually set it
$_FILES = $form->getPhpFiles();
$this->client->submit($form);
$this->assertResponseRedirects('/m/acme/default/newest');
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('article h2', 'Test image 1');
$this->assertSelectorExists('figure img');
$imgSrc = $crawler->filter('figure img.thumb-subject')->getNode(0)->attributes->getNamedItem('src')->textContent;
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $imgSrc);
$user = $this->getUserByUsername('user');
$entry = $repository->findOneBy(['user' => $user]);
$this->assertNotNull($entry);
$this->assertNotNull($entry->image);
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $entry->image->filePath);
$_FILES = [];
}
public function testUserCanCreateEntryArticleForAdults()
{
$this->client->loginUser($this->getUserByUsername('user', hideAdult: false));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/new_entry');
$this->client->submit(
$crawler->filter('form[name=entry]')->selectButton('Add new thread')->form(
[
'entry[title]' => 'Test entry 1',
'entry[body]' => 'Test body',
'entry[isAdult]' => '1',
]
)
);
$this->assertResponseRedirects('/m/acme/default/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('article h2', 'Test entry 1');
$this->assertSelectorTextContains('article h2 .danger', '18+');
}
public function testPresetValues()
{
$this->client->loginUser($this->getUserByUsername('user', hideAdult: false));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET',
'/m/acme/new_entry?'
.'title=test'
.'&url='.urlencode('https://example.com#title')
.'&body='.urlencode("**Test**\nbody")
.'&imageAlt=alt'
.'&isNsfw=1'
.'&isOc=1'
.'&tags[]=1&tags[]=2'
);
$this->assertFormValue('form[name=entry]', 'entry[title]', 'test');
$this->assertFormValue('form[name=entry]', 'entry[url]', 'https://example.com#title');
$this->assertFormValue('form[name=entry]', 'entry[body]', "**Test**\nbody");
$this->assertFormValue('form[name=entry]', 'entry[imageAlt]', 'alt');
$this->assertFormValue('form[name=entry]', 'entry[isAdult]', '1');
$this->assertFormValue('form[name=entry]', 'entry[isOc]', '1');
$this->assertFormValue('form[name=entry]', 'entry[tags]', '1 2');
}
public function testPresetImage()
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$magazine = $this->getMagazineByName('acme');
$imgEntry = $this->createEntry('img', $magazine, $user, imageDto: $this->getKibbyImageDto());
$imgHash = strtok($imgEntry->image->fileName, '.');
// this is necessary so the second entry is guaranteed to be newer than the first
sleep(1);
$crawler = $this->client->request('GET',
'/m/acme/new_entry?'
.'title=test'
.'&imageHash='.$imgHash
);
$this->client->submit($crawler->filter('form[name=entry]')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('article h2', 'test');
$this->assertSelectorExists('figure img');
$imgSrc = $crawler->filter('figure img.thumb-subject')->getNode(0)->attributes->getNamedItem('src')->textContent;
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $imgSrc);
}
public function testPresetImageNotFound()
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$magazine = $this->getMagazineByName('acme');
$imgEntry = $this->createEntry('img', $magazine, $user, imageDto: $this->getKibbyImageDto());
$imgHash = strtok($imgEntry->image->fileName, '.');
$imgHash = substr($imgHash, 0, \strlen($imgHash) - 1).'0';
// this is necessary so the second entry is guaranteed to be newer than the first
sleep(1);
$crawler = $this->client->request('GET',
'/m/acme/new_entry?'
.'title=test'
.'&imageHash='.$imgHash
);
$this->assertSelectorTextContains('.alert.alert__danger', 'The image referenced by \'imageHash\' could not be found.');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryDeleteControllerTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$entry = $this->getEntryByTitle('deletion test', body: 'will be deleted', magazine: $magazine, user: $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/m/acme');
$this->assertSelectorExists('form[action$="delete"]');
$this->client->submit(
$crawler->filter('form[action$="delete"]')->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
}
public function testUserCanSoftDeleteEntry()
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$entry = $this->getEntryByTitle('deletion test', body: 'will be deleted', magazine: $magazine, user: $user);
$comment = $this->createEntryComment('only softly', $entry, $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/m/acme');
$this->assertSelectorExists('form[action$="delete"]');
$this->client->submit(
$crawler->filter('form[action$="delete"]')->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/deletion-test");
$translator = $this->translator;
$this->assertSelectorTextContains("#entry-{$entry->getId()} header", $translator->trans('deleted_by_author'));
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryEditControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .entry_edit');
$this->assertInputValueSame('entry_edit[url]', 'https://kbin.pub');
$this->assertEquals('disabled', $crawler->filter('#entry_edit_magazine')->attr('disabled'));
$this->client->submit(
$crawler->filter('form[name=entry_edit]')->selectButton('Edit thread')->form(
[
'entry_edit[title]' => 'test entry 2 title',
'entry_edit[body]' => 'test entry 2 body',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry header', 'test entry 2 title');
$this->assertSelectorTextContains('#main .entry .entry__body', 'test entry 2 body');
}
public function testAuthorCanEditOwnEntryArticle(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', null, 'entry content test entry 1');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .entry_edit');
$this->assertEquals('disabled', $crawler->filter('#entry_edit_magazine')->attr('disabled'));
$this->client->submit(
$crawler->filter('form[name=entry_edit]')->selectButton('Edit thread')->form(
[
'entry_edit[title]' => 'test entry 2 title',
'entry_edit[body]' => 'test entry 2 body',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry header', 'test entry 2 title');
$this->assertSelectorTextContains('#main .entry .entry__body', 'test entry 2 body');
}
#[Group(name: 'NonThreadSafe')]
public function testAuthorCanEditOwnEntryImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$imageDto = $this->getKibbyImageDto();
$entry = $this->getEntryByTitle('test entry 1', image: $imageDto);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .entry')->selectLink('Edit')->link());
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('#main .entry_edit');
$this->assertSelectorExists('#main .entry_edit img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
$this->assertEquals('disabled', $crawler->filter('#entry_edit_magazine')->attr('disabled'));
$this->client->submit(
$crawler->filter('form[name=entry_edit]')->selectButton('Edit thread')->form(
[
'entry_edit[title]' => 'test entry 2 title',
]
)
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .entry header', 'test entry 2 title');
$this->assertSelectorExists('#main .entry img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryFrontControllerTest.php
================================================
client = $this->prepareEntries();
$this->client->request('GET', '/');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/newest');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextContains('.entry__meta', 'to acme');
$this->assertcount(2, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testXmlRootPage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/');
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testXmlRootPageIsFrontPage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/');
$root_content = self::removeTimeElements($this->clearTokens($this->client->getResponse()->getContent()));
$this->client->request('GET', '/all');
$frontContent = self::removeTimeElements($this->clearTokens($this->client->getResponse()->getContent()));
$this->assertSame($root_content, $frontContent);
}
public function testFrontPage(): void
{
$this->client = $this->prepareEntries();
$this->client->request('GET', '/all');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/all/newest');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextContains('.entry__meta', 'to acme');
$this->assertcount(2, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testXmlFrontPage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/all');
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testMagazinePage(): void
{
$this->client = $this->prepareEntries();
$this->client->request('GET', '/m/acme');
$this->assertSelectorTextContains('h2', 'Hot');
$this->client->request('GET', '/m/ACME');
$this->assertSelectorTextContains('h2', 'Hot');
$crawler = $this->client->request('GET', '/m/acme/threads/newest');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextNotContains('.entry__meta', 'to acme');
$this->assertSelectorTextContains('.head-title', '/m/acme');
$this->assertSelectorTextContains('#sidebar .magazine', 'acme');
$this->assertSelectorTextContains('#header .active', 'Threads');
$this->assertcount(1, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', 'acme');
$this->assertSelectorTextContains('h2', ucfirst($sortOption));
}
}
public function testXmlMagazinePage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/m/acme/newest');
self::assertResponseIsSuccessful();
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testSubPage(): void
{
$this->client = $this->prepareEntries();
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$magazineManager->subscribe($this->getMagazineByName('acme'), $this->getUserByUsername('Actor'));
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/sub');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/sub/threads/newest');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextContains('.entry__meta', 'to acme');
$this->assertSelectorTextContains('.head-title', '/sub');
$this->assertSelectorTextContains('#header .active', 'Threads');
$this->assertcount(1, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testXmlSubPage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$magazineManager->subscribe($this->getMagazineByName('acme'), $this->getUserByUsername('Actor'));
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/sub');
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testModPage(): void
{
$this->client = $this->prepareEntries();
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$moderator = new ModeratorDto($this->getMagazineByName('acme'));
$moderator->user = $this->getUserByUsername('Actor');
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/mod');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/mod/threads/newest');
$this->assertSelectorTextContains('.entry__meta', 'JohnDoe');
$this->assertSelectorTextContains('.entry__meta', 'to acme');
$this->assertSelectorTextContains('.head-title', '/mod');
$this->assertSelectorTextContains('#header .active', 'Threads');
$this->assertcount(1, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testXmlModPage(): void
{
$admin = $this->getUserByUsername('admin', isAdmin: true);
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$moderator = new ModeratorDto($this->getMagazineByName('acme'));
$moderator->user = $this->getUserByUsername('Actor');
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/mod');
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testFavPage(): void
{
$this->client = $this->prepareEntries();
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle(
$this->getUserByUsername('Actor'),
$this->getEntryByTitle('test entry 1', 'https://kbin.pub')
);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/fav');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/fav/threads/newest');
$this->assertSelectorTextContains('.entry__meta', 'JaneDoe');
$this->assertSelectorTextContains('.entry__meta', 'to kbin');
$this->assertSelectorTextContains('.head-title', '/fav');
$this->assertSelectorTextContains('#header .active', 'Threads');
$this->assertcount(1, $crawler->filter('.entry'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testXmlFavPage(): void
{
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle(
$this->getUserByUsername('Actor'),
$this->getEntryByTitle('test entry 1', 'https://kbin.pub')
);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->request('GET', '/fav');
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testCustomDefaultSort(): void
{
$older = $this->getEntryByTitle('Older entry');
$older->createdAt = new \DateTimeImmutable('now - 1 day');
$older->updateRanking();
$this->entityManager->flush();
$newer = $this->getEntryByTitle('Newer entry');
$comment = $this->createEntryComment('someone was here', entry: $older);
self::assertGreaterThan($older->getRanking(), $newer->getRanking());
$user = $this->getUserByUsername('user');
$user->frontDefaultSort = ESortOptions::Newest->value;
$this->entityManager->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('.options__filter button', $this->translator->trans(ESortOptions::Newest->value));
$iterator = $crawler->filter('#content div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-{$newer->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-{$older->getId()}", $secondId);
$user->frontDefaultSort = ESortOptions::Commented->value;
$this->entityManager->flush();
$crawler = $this->client->request('GET', '/');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('.options__filter button', $this->translator->trans(ESortOptions::Commented->value));
$iterator = $crawler->filter('#content div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-{$older->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-{$newer->getId()}", $secondId);
}
private function prepareEntries(): KernelBrowser
{
$older = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
$this->getMagazineByName('kbin', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
$older->createdAt = new \DateTimeImmutable('now - 1 minute');
$this->getEntryByTitle('test entry 2', 'https://kbin.pub');
return $this->client;
}
private function getSortOptions(): array
{
return ['Top', 'Hot', 'Newest', 'Active', 'Commented'];
}
private function clearTokens(string $responseContent): string
{
return preg_replace(
'#name="token" value=".+"#',
'',
json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR),
)['html'];
}
private function clearDateTimes(string $responseContent): string
{
return preg_replace(
'/[ \w\n]*<\/time>/m',
'',
$responseContent
);
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryLockControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Lock')->form([]));
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#main .entry footer span .fa-lock');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .entry footer span .fa-lock');
}
public function testAuthorCanLockEntry(): void
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub', user: $user);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}");
$this->assertSelectorExists('#main .entry footer .dropdown .fa-lock');
$this->client->submit($crawler->filter('#main .entry footer .dropdown')->selectButton('Lock')->form([]));
$this->client->followRedirect();
$this->assertSelectorExists('#main .entry footer span .fa-lock');
}
public function testModCanUnlockEntry(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->entryManager->toggleLock($entry, $user);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->assertSelectorExists('#main .entry footer span .fa-lock');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .entry footer span .fa-lock');
}
public function testAuthorCanUnlockEntry(): void
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub', user: $user);
$this->entryManager->toggleLock($entry, $user);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}");
$this->assertSelectorExists('#main .entry footer span .fa-lock');
$this->client->submit($crawler->filter('#main .entry footer .dropdown')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .entry footer span .fa-lock');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryModerateControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('get', '/');
$this->client->click($crawler->filter('#entry-'.$entry->getId())->selectLink('Moderate')->link());
$this->assertSelectorTextContains('.moderate-panel', 'ban');
}
public function testXmlModCanShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('get', '/');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('#entry-'.$entry->getId())->selectLink('Moderate')->link());
$this->assertStringContainsString('moderate-panel', $this->client->getResponse()->getContent());
}
public function testUnauthorizedCanNotShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->request('get', "/m/{$entry->magazine->name}/t/{$entry->getId()}");
$this->assertSelectorTextNotContains('#entry-'.$entry->getId(), 'Moderate');
$this->client->request(
'get',
"/m/{$entry->magazine->name}/t/{$entry->getId()}/-/moderate"
);
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryPinControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/-/moderate");
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Pin')->form([]));
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#main .entry .fa-thumbtack');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unpin')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .entry .fa-thumbtack');
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntrySingleControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/');
$this->client->click($crawler->selectLink('test entry 1')->link());
$this->assertSelectorTextContains('.head-title', '/m/acme');
$this->assertSelectorTextContains('#header nav .active', 'Threads');
$this->assertSelectorTextContains('article h1', 'test entry 1');
$this->assertSelectorTextContains('#main', 'No comments');
$this->assertSelectorTextContains('#sidebar .entry-info', 'Thread');
$this->assertSelectorTextContains('#sidebar .magazine', 'Magazine');
$this->assertSelectorTextContains('#sidebar .user-list', 'Moderators');
}
public function testUserCanSeeArticle(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', null, 'Test entry content');
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->assertSelectorTextContains('article h1', 'test entry 1');
$this->assertSelectorNotExists('article h1 > a');
$this->assertSelectorTextContains('article', 'Test entry content');
}
public function testUserCanSeeLink(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->assertSelectorExists('article h1 a[href="https://kbin.pub"]', 'test entry 1');
}
public function testPostActivityCounter(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$manager = $this->client->getContainer()->get(VoteManager::class);
$manager->vote(VotableInterface::VOTE_DOWN, $entry, $this->getUserByUsername('JaneDoe'));
$manager = $this->client->getContainer()->get(FavouriteManager::class);
$manager->toggle($this->getUserByUsername('JohnDoe'), $entry);
$manager->toggle($this->getUserByUsername('JaneDoe'), $entry);
$this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->assertSelectorTextContains('.options-activity', 'Activity (2)');
}
public function testCanSortComments()
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$this->createEntryComment('test comment 1', $entry);
$this->createEntryComment('test comment 2', $entry);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
}
}
public function testCommentsDefaultSortOption(): void
{
$user = $this->getUserByUsername('user');
$entry = $this->getEntryByTitle('entry');
$older = $this->createEntryComment('older comment', entry: $entry);
$older->createdAt = new \DateTimeImmutable('now - 1 day');
$newer = $this->createEntryComment('newer comment', entry: $entry);
$user->commentDefaultSort = ESortOptions::Oldest->value;
$this->entityManager->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/{$entry->magazine->name}/t/{$entry->getId()}/-");
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('.options__filter .active', $this->translator->trans(ESortOptions::Oldest->value));
$iterator = $crawler->filter('#comments div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-comment-{$older->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-comment-{$newer->getId()}", $secondId);
$user->commentDefaultSort = ESortOptions::Newest->value;
$this->entityManager->flush();
$crawler = $this->client->request('GET', "/m/{$entry->magazine->name}/t/{$entry->getId()}/-");
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('.options__filter .active', $this->translator->trans(ESortOptions::Newest->value));
$iterator = $crawler->filter('#comments div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-comment-{$newer->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("entry-comment-{$older->getId()}", $secondId);
}
private function getSortOptions(): array
{
return ['Top', 'Hot', 'Newest', 'Active', 'Oldest'];
}
}
================================================
FILE: tests/Functional/Controller/Entry/EntryVotersControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$manager = $this->client->getContainer()->get(VoteManager::class);
$manager->vote(VotableInterface::VOTE_UP, $entry, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$this->client->click($crawler->filter('.options-activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JaneDoe');
}
public function testUserCannotSeeDownVoters(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$manager = $this->client->getContainer()->get(VoteManager::class);
$manager->vote(VotableInterface::VOTE_DOWN, $entry, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $crawler->filter('.options-activity')->selectLink('Reduces (1)');
self::assertEquals(0, $crawler->count());
$this->assertSelectorTextContains('.options-activity', 'Reduces (1)');
}
}
================================================
FILE: tests/Functional/Controller/Magazine/MagazineBlockControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
// Block magazine
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=magazine_block] .active');
// Unblock magazine
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$this->client->followRedirect();
$this->assertSelectorNotExists('#sidebar form[name=magazine_block] .active');
}
public function testXmlUserCanBlockMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('active', $this->client->getResponse()->getContent());
}
public function testXmlUserCanUnblockMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
// Block magazine
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$crawler = $this->client->followRedirect();
// Unblock magazine
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=magazine_block] button')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringNotContainsString('active', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/Magazine/MagazineCreateControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/newMagazine');
$this->client->submit(
$crawler->filter('form[name=magazine]')->selectButton('Create new magazine')->form(
[
'magazine[name]' => 'TestMagazine',
'magazine[title]' => 'Test magazine title',
]
)
);
$this->assertResponseRedirects('/m/TestMagazine');
$this->client->followRedirect();
$this->assertSelectorTextContains('.head-title', '/m/TestMagazine');
$this->assertSelectorTextContains('#content', 'Empty');
}
public function testUserCantCreateInvalidMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/newMagazine');
$this->client->submit(
$crawler->filter('form[name=magazine]')->selectButton('Create new magazine')->form(
[
'magazine[name]' => 't',
'magazine[title]' => 'Test magazine title',
]
)
);
$this->assertSelectorTextContains('#content', 'This value is too short. It should have 2 characters or more.');
}
public function testUserCantCreateTwoSameMagazines(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/newMagazine');
$this->client->submit(
$crawler->filter('form[name=magazine]')->selectButton('Create new magazine')->form(
[
'magazine[name]' => 'acme',
'magazine[title]' => 'Test magazine title',
]
)
);
$this->assertSelectorTextContains('#content', 'This value is already used.');
}
}
================================================
FILE: tests/Functional/Controller/Magazine/MagazineListControllerTest.php
================================================
loadExampleMagazines();
$crawler = $this->client->request('GET', '/magazines');
$crawler = $this->client->submit(
$crawler->filter('form[method=get]')->selectButton('')->form($queryParams)
);
$actualMagazines = $crawler->filter('#content .table-responsive .magazine-inline')->each(fn (Crawler $node) => $node->innerText());
$this->assertSame(
sort($expectedMagazines),
sort($actualMagazines),
);
}
public static function magazines(): iterable
{
return [
[['query' => 'test'], []],
[['query' => 'acme'], ['Magazyn polityczny']],
[['query' => '', 'adult' => 'only'], ['Adult only']],
[['query' => 'acme', 'adult' => 'only'], []],
[['query' => 'foobar', 'fields' => 'names_descriptions'], ['Magazyn polityczny']],
[['adult' => 'show'], ['Magazyn polityczny', 'kbin devlog', 'Adult only', 'starwarsmemes@republic.new']],
[['federation' => 'local'], ['Magazyn polityczny', 'kbin devlog', 'Adult only']],
[['query' => 'starwars', 'federation' => 'local'], []],
[['query' => 'starwars', 'federation' => 'all'], ['starwarsmemes@republic.new']],
[['query' => 'trap', 'fields' => 'names_descriptions'], ['starwarsmemes@republic.new']],
];
}
}
================================================
FILE: tests/Functional/Controller/Magazine/MagazinePeopleControllerTest.php
================================================
getUserByUsername('JohnDoe');
$this->createPost('test post content');
$user->about = 'Loerm ipsum';
$this->entityManager->flush();
$crawler = $this->client->request('GET', '/m/acme/people');
$this->assertEquals(1, $crawler->filter('#main .user-box')->count());
$this->assertSelectorTextContains('#main .users .user-box', 'Loerm ipsum');
}
}
================================================
FILE: tests/Functional/Controller/Magazine/MagazineSubControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
// Sub magazine
$this->client->submit($crawler->filter('#sidebar .magazine')->selectButton('Subscribe')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=magazine_subscribe] .active');
$this->assertSelectorTextContains('#sidebar .magazine', 'Unsubscribe');
$this->assertSelectorTextContains('#sidebar .magazine', '2');
// Unsub magazine
$this->client->submit($crawler->filter('#sidebar .magazine')->selectButton('Unsubscribe')->form());
$this->client->followRedirect();
$this->assertSelectorTextContains('#sidebar .magazine', '1');
}
public function testXmlUserCanSubMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .magazine')->selectButton('Subscribe')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Unsubscribe', $this->client->getResponse()->getContent());
}
public function testXmlUserCanUnsubMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme');
// Sub magazine
$this->client->submit($crawler->filter('#sidebar .magazine')->selectButton('Subscribe')->form());
$crawler = $this->client->followRedirect();
// Unsub magazine
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .magazine')->selectButton('Unsubscribe')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Subscribe', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineAppearanceControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/panel/appearance');
$this->assertSelectorTextContains('#main .options__main a.active', 'Appearance');
$form = $crawler->filter('#main form[name=magazine_theme]')->selectButton('Done')->form();
$form['magazine_theme[icon]']->upload($this->kibbyPath);
$crawler = $this->client->submit($form);
$this->assertResponseIsSuccessful();
$this->assertSelectorExists('#sidebar .magazine img');
$node = $crawler->filter('#sidebar .magazine img')->getNode(0);
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $node->attributes->getNamedItem('src')->textContent);
}
public function testOwnerCanEditMagazineCSS(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/panel/appearance');
$this->assertSelectorTextContains('#main .options__main a.active', 'Appearance');
$form = $crawler->filter('#main form[name=magazine_theme]')->selectButton('Done')->form();
$form['magazine_theme[customCss]']->setValue('#middle { display: none; }');
$crawler = $this->client->submit($form);
$this->assertResponseIsSuccessful();
}
public function testUnauthorizedUserCannotEditMagazineTheme(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/appearance');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineBadgeControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
// Add badge
$crawler = $this->client->request('GET', '/m/acme/panel/badges');
$this->assertSelectorTextContains('#main .options__main a.active', 'Badges');
$this->client->submit(
$crawler->filter('#main form[name=badge]')->selectButton('Add badge')->form([
'badge[name]' => 'test',
])
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .badges', 'test');
// Remove badge
$this->client->submit(
$crawler->filter('#main .badges')->selectButton('Delete')->form()
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .section--muted', 'Empty');
}
public function testUnauthorizedUserCannotAddBadge(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/badges');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineBanControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getUserByUsername('JaneDoe');
$this->getMagazineByName('acme');
// Add ban
$crawler = $this->client->request('GET', '/m/acme/panel/bans');
$this->assertSelectorTextContains('#main .options__main a.active', 'Bans');
$crawler = $this->client->submit(
$crawler->filter('#main form[name=ban]')->selectButton('Add ban')->form([
'username' => 'JaneDoe',
])
);
$this->client->submit(
$crawler->filter('#main form[name=magazine_ban]')->selectButton('Ban')->form([
'magazine_ban[reason]' => 'Reason test',
'magazine_ban[expiredAt]' => (new \DateTimeImmutable('+2 weeks'))->format('Y-m-d H:i:s'),
])
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .bans-table', 'JaneDoe');
// Remove ban
$this->client->submit(
$crawler->filter('#main .bans-table')->selectButton('Delete')->form()
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main', 'Empty');
}
public function testUnauthorizedUserCannotAddBan(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/bans');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineEditControllerTest.php
================================================
getUserByUsername('JohnDoe');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$this->client->loginUser($mod);
$magazine = $this->getMagazineByName('acme', $admin);
$manager = $this->magazineManager;
$dto = new ModeratorDto($magazine, $mod, $admin);
$manager->addModerator($dto);
$this->client->request('GET', '/m/acme');
$this->assertSelectorTextNotContains('#sidebar .magazine', 'Magazine panel');
}
public function testOwnerCanEditMagazine(): void
{
$owner = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($owner);
$magazine = $this->getMagazineByName('acme', $owner);
$magazine->rules = 'init rules';
$this->entityManager->persist($magazine);
$this->entityManager->flush();
$crawler = $this->client->request('GET', '/m/acme/panel/general');
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('#main .options__main a.active', 'General');
$this->client->submit(
$crawler->filter('#main form[name=magazine]')->selectButton('Done')->form([
'magazine[description]' => 'test description edit',
'magazine[rules]' => 'test rules edit',
'magazine[isAdult]' => true,
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#sidebar .magazine', 'test description edit');
$this->assertSelectorTextContains('#sidebar .magazine', 'test rules edit');
}
public function testCannotEditRulesWhenEmpty(): void
{
$owner = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($owner);
$this->getMagazineByName('acme', $owner);
$crawler = $this->client->request('GET', '/m/acme/panel/general');
self::assertResponseIsSuccessful();
$exception = null;
try {
$crawler->filter('#main form[name=magazine]')->selectButton('Done')->form([
'magazine[rules]' => 'test rules edit',
]);
} catch (\Exception $e) {
$exception = $e;
}
self::assertNotNull($exception);
self::assertStringContainsString('Unreachable field "rules"', $exception->getMessage());
}
public function testUnauthorizedUserCannotEditMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/general');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineModeratorControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getUserByUsername('JaneDoe');
$this->getMagazineByName('acme');
// Add moderator
$crawler = $this->client->request('GET', '/m/acme/panel/moderators');
$this->assertSelectorTextContains('#main .options__main a.active', 'Moderators');
$crawler = $this->client->submit(
$crawler->filter('#main form[name=moderator]')->selectButton('Add moderator')->form([
'moderator[user]' => 'JaneDoe',
])
);
$this->assertSelectorTextContains('#main .users-columns', 'JaneDoe');
$this->assertEquals(2, $crawler->filter('#main .users-columns ul li')->count());
// Remove moderator
$this->client->submit(
$crawler->filter('#main .users-columns')->selectButton('Delete')->form()
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextNotContains('#main .users-columns', 'JaneDoe');
$this->assertEquals(1, $crawler->filter('#main .users-columns ul li')->count());
}
public function testUnauthorizedUserCannotAddModerator(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/moderators');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineReportControllerTest.php
================================================
client->loginUser($user = $this->getUserByUsername('JohnDoe'));
$user2 = $this->getUserByUsername('JaneDoe');
$entryComment = $this->createEntryComment('Test comment 1');
$postComment = $this->createPostComment('Test post 1');
foreach ([$entryComment, $postComment, $entryComment->entry, $postComment->post] as $subject) {
$this->reportManager->report(
ReportDto::create($subject, 'test reason'),
$user
);
}
$this->client->request('GET', '/');
$crawler = $this->client->request('GET', '/m/acme/panel/reports');
$this->assertSelectorTextContains('#main .options__main a.active', 'Reports');
$this->assertEquals(
4,
$crawler->filter('#main .report')->count()
);
}
public function testUnauthorizedUserCannotSeeReports(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/reports');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Magazine/Panel/MagazineTrashControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$entry = $this->getEntryByTitle(
'Test entry 1',
'https://kbin.pub',
null,
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId().'/test-entry-1/moderate');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Delete')->form([]));
$this->client->request('GET', '/m/acme/panel/trash');
$this->assertSelectorTextContains('#main .options__main a.active', 'Trash');
$this->assertSelectorTextContains('#main .entry', 'Test entry 1');
}
public function testModCanSeeEntryCommentInTrash(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$comment = $this->createEntryComment(
'Test comment 1',
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request(
'GET',
'/m/acme/t/'.$comment->entry->getId().'/test-entry-1/comment/'.$comment->getId().'/moderate'
);
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Delete')->form([]));
$this->client->request('GET', '/m/acme/panel/trash');
$this->assertSelectorTextContains('#main .comment', 'Test comment 1');
}
public function testModCanSeePostInTrash(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$post = $this->createPost(
'Test post 1',
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request(
'GET',
'/m/acme/p/'.$post->getId().'/-/moderate'
);
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Delete')->form([]));
$this->client->request('GET', '/m/acme/panel/trash');
$this->assertSelectorTextContains('#main .post', 'Test post 1');
}
public function testModCanSeePostCommentInTrash(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$comment = $this->createPostComment(
'Test comment 1',
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request(
'GET',
'/m/acme/p/'.$comment->post->getId().'/test-entry-1/reply/'.$comment->getId().'/moderate'
);
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Delete')->form([]));
$this->client->request('GET', '/m/acme/panel/trash');
$this->assertSelectorTextContains('#main .comment', 'Test comment 1');
}
public function testUnauthorizedUserCannotSeeTrash(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$this->getMagazineByName('acme');
$this->client->request('GET', '/m/acme/panel/trash');
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Moderator/ModeratorSignupRequestsControllerTest.php
================================================
settingsManager->set('MBIN_NEW_USERS_NEED_APPROVAL', true);
$this->client->loginUser($this->getUserByUsername('moderator', isModerator: true));
$crawler = $this->client->request('GET', '/');
$this->client->click($crawler->filter('#header menu')->selectLink('Signup requests')->link());
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('#main h3', 'Signup Requests');
}
}
================================================
FILE: tests/Functional/Controller/People/FrontControllerTest.php
================================================
getUserByUsername('JohnDoe');
$user->about = 'Loerm ipsum';
$this->entityManager->flush();
$crawler = $this->client->request('GET', '/people');
$this->assertEquals(1, $crawler->filter('#main .user-box')->count());
$this->assertSelectorTextContains('#main .users .user-box', 'Loerm ipsum');
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentBoostControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1', null, $this->getUserByUsername('JaneDoe'));
$comment = $this->createPostComment('test comment 1', $post, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->submit(
$crawler->filter("#post-comment-{$comment->getId()}")->selectButton('Boost')->form()
);
$crawler = $this->client->followRedirect();
self::assertResponseIsSuccessful();
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
// $this->assertSelectorTextContains("#post-comment-{$comment->getId()}", 'Boost (1)');
$crawler = $this->client->click($crawler->filter("#post-comment-{$comment->getId()}")->selectLink('Activity')->link());
$this->assertSelectorTextContains('#main #activity', 'Boosts (1)');
$this->client->click($crawler->filter('#main #activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JohnDoe');
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentChangeLangControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createPostComment('test comment 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$comment->post->getId()}/-/reply/{$comment->getId()}/moderate");
$form = $crawler->filter('.moderate-panel')->selectButton('Change language')->form();
$this->assertSame($form['lang']['lang']->getValue(), 'en');
$form['lang']['lang']->select('fr');
$this->client->submit($form);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .badge-lang', 'French');
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentCreateControllerTest.php
================================================
kibbyPath = \dirname(__FILE__, 5).'/assets/kibby_emoji.png';
}
public function testUserCanCreatePostComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', '/m/acme/p/'.$post->getId().'/test-post-1/reply');
$this->client->submit(
$crawler->filter('form[name=post_comment]')->selectButton('Add comment')->form(
[
'post_comment[body]' => 'test comment 1',
]
)
);
$this->assertResponseRedirects('/m/acme/p/'.$post->getId().'/test-post-1');
$this->client->followRedirect();
$this->assertSelectorTextContains('#comments .content', 'test comment 1');
}
public function testUserCannotCreatePostCommentInLockedPost(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$post = $this->createPost('test post 1');
$this->postManager->toggleLock($post, $user);
$crawler = $this->client->request('GET', '/m/acme/p/'.$post->getId().'/test-post-1/reply');
$this->client->submit(
$crawler->filter('form[name=post_comment]')->selectButton('Add comment')->form(
[
'post_comment[body]' => 'test comment 1',
]
)
);
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('#main', 'Failed to create comment');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanCreatePostCommentWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1/reply");
$form = $crawler->filter('form[name=post_comment]')->selectButton('Add comment')->form();
$form->get('post_comment[body]')->setValue('Test comment 1');
$form->get('post_comment[image]')->upload($this->kibbyPath);
// Needed since we require this global to be set when validating entries but the client doesn't actually set it
$_FILES = $form->getPhpFiles();
$this->client->submit($form);
$this->assertResponseRedirects("/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#comments .content', 'Test comment 1');
$this->assertSelectorExists('#comments footer figure img');
$imgSrc = $crawler->filter('#comments footer figure img')->getNode(0)->attributes->getNamedItem('src')->textContent;
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $imgSrc);
$_FILES = [];
}
#[Group(name: 'NonThreadSafe')]
public function testUserCannotCreateInvalidPostComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', '/m/acme/p/'.$post->getId().'/test-post-1/reply');
$crawler = $this->client->submit(
$crawler->filter('form[name=post_comment]')->selectButton('Add comment')->form(
[
'post_comment[body]' => '',
]
)
);
$this->assertSelectorTextContains('#content', 'This value should not be blank.');
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentDeleteControllerTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByName('acme', $user);
$post = $this->createPost('deletion test', magazine: $magazine, user: $user);
$comment = $this->createPostComment('delete me!', $post, $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
self::assertResponseIsSuccessful();
$link = $crawler->filter('#comments .post-comment footer')->selectLink('Moderate')->link();
$crawler = $this->client->click($link);
self::assertResponseIsSuccessful();
$this->assertSelectorNotExists('.moderate-panel form[action$="purge"]');
}
public function testAdminCanPurgePostComment()
{
$user = $this->getUserByUsername('user');
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazine = $this->getMagazineByName('acme', $user);
$post = $this->createPost('deletion test', magazine: $magazine, user: $user);
$comment = $this->createPostComment('delete me!', $post, $user);
$this->client->loginUser($admin);
self::assertTrue($admin->isAdmin());
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
self::assertResponseIsSuccessful();
$link = $crawler->filter("#comments #post-comment-{$comment->getId()} footer")->selectLink('Moderate')->link();
$crawler = $this->client->click($link);
self::assertResponseIsSuccessful();
self::assertSelectorExists('.moderate-panel');
$this->assertSelectorExists('.moderate-panel form[action$="purge"]');
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Purge')->form()
);
$this->assertResponseRedirects();
}
public function testUserCanSoftDeletePostComment()
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme', $user);
$post = $this->createPost('deletion test', magazine: $magazine, user: $user);
$comment = $this->createPostComment('delete me!', $post, $user);
$reply = $this->createPostCommentReply('Are you deleted yet?', $post, $user, $comment);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
self::assertResponseIsSuccessful();
$link = $crawler->filter('#comments .post-comment footer')->selectLink('Moderate')->link();
$crawler = $this->client->click($link);
self::assertResponseIsSuccessful();
$this->assertSelectorExists('.moderate-panel form[action$="delete"]');
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
$this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
$translator = $this->translator;
$this->assertSelectorTextContains("#post-comment-{$comment->getId()} .content", $translator->trans('deleted_by_author'));
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentEditControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$this->createPostComment('test comment 1', $post);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post-comment')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .post-comment');
$this->assertSelectorTextContains('textarea[name="post_comment[body]"]', 'test comment 1');
$this->client->submit(
$crawler->filter('form[name=post_comment]')->selectButton('Save changes')->form(
[
'post_comment[body]' => 'test comment 2 body',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .post-comment', 'test comment 2 body');
}
#[Group(name: 'NonThreadSafe')]
public function testAuthorCanEditOwnPostCommentWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$imageDto = $this->getKibbyImageDto();
$this->createPostComment('test comment 1', $post, imageDto: $imageDto);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post-comment')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .post-comment');
$this->assertSelectorTextContains('textarea[name="post_comment[body]"]', 'test comment 1');
$this->assertSelectorExists('#main .post-comment img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
$this->client->submit(
$crawler->filter('form[name=post_comment]')->selectButton('Save changes')->form(
[
'post_comment[body]' => 'test comment 2 body',
]
)
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .post-comment', 'test comment 2 body');
$this->assertSelectorExists('#main .post-comment img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
}
}
================================================
FILE: tests/Functional/Controller/Post/Comment/PostCommentModerateControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createPostComment('test comment 1');
$crawler = $this->client->request('get', "/m/{$comment->magazine->name}/p/{$comment->post->getId()}");
$this->client->click($crawler->filter('#post-comment-'.$comment->getId())->selectLink('Moderate')->link());
$this->assertSelectorTextContains('.moderate-panel', 'ban');
}
public function testXmlModCanShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$comment = $this->createPostComment('test comment 1');
$crawler = $this->client->request('get', "/m/{$comment->magazine->name}/p/{$comment->post->getId()}");
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('#post-comment-'.$comment->getId())->selectLink('Moderate')->link());
$this->assertStringContainsString('moderate-panel', $this->client->getResponse()->getContent());
}
public function testUnauthorizedCanNotShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$comment = $this->createPostComment('test comment 1');
$this->client->request('get', "/m/{$comment->magazine->name}/p/{$comment->post->getId()}");
$this->assertSelectorTextNotContains('#post-comment-'.$comment->getId(), 'moderate');
$this->client->request(
'get',
"/m/{$comment->magazine->name}/p/{$comment->post->getId()}/-/reply/{$comment->getId()}/moderate"
);
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Post/PostBoostControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1', null, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$this->client->submit(
$crawler->filter('#main .post')->selectButton('Boost')->form([])
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .post', 'Boost (1)');
$this->client->click($crawler->filter('#activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JohnDoe');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostChangeAdultControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Mark NSFW')->form([
'adult' => 'on',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .post .badge', '18+');
$this->client->submit(
$crawler->filter('.moderate-panel')->selectButton('Mark NSFW')->form([
'adult' => false,
])
);
$this->client->followRedirect();
$this->assertSelectorTextNotContains('#main .post', '18+');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostChangeLangControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$form = $crawler->filter('.moderate-panel')->selectButton('Change language')->form();
$this->assertSame($form['lang']['lang']->getValue(), 'en');
$form['lang']['lang']->select('fr');
$this->client->submit($form);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .badge-lang', 'French');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostChangeMagazineControllerTest.php
================================================
getUserByUsername('JohnDoe');
$this->setAdmin($user);
$this->client->loginUser($user);
$this->getMagazineByName('kbin');
$post = $this->createPost(
'test post 1',
);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$this->client->submit(
$crawler->filter('form[name=change_magazine]')->selectButton('Change magazine')->form(
[
'change_magazine[new_magazine]' => 'kbin',
]
)
);
$this->client->followRedirect();
$this->client->followRedirect();
$this->assertSelectorTextContains('.head-title', 'kbin');
}
public function testUnauthorizedUserCantChangeMagazine(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('kbin');
$entry = $this->createPost(
'test post 1',
);
$this->client->request('GET', "/m/acme/p/{$entry->getId()}/-/moderate");
$this->assertSelectorTextNotContains('.moderate-panel', 'Change magazine');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostCreateControllerTest.php
================================================
kibbyPath = \dirname(__FILE__, 4).'/assets/kibby_emoji.png';
}
public function testUserCanCreatePost(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/microblog');
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Add post')->form(
[
'post[body]' => 'test post 1',
]
)
);
$this->assertResponseRedirects('/m/acme/microblog/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('#content .post', 'test post 1');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanCreatePostWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/microblog');
$form = $crawler->filter('form[name=post]')->selectButton('Add post')->form();
$form->get('post[body]')->setValue('test post 1');
$form->get('post[image]')->upload($this->kibbyPath);
// Needed since we require this global to be set when validating entries but the client doesn't actually set it
$_FILES = $form->getPhpFiles();
$this->client->submit($form);
$this->assertResponseRedirects('/m/acme/microblog/newest');
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#content .post', 'test post 1');
$this->assertSelectorExists('#content .post div.content figure img');
$imgSrc = $crawler->filter('#content .post div.content figure img')->getNode(0)->attributes->getNamedItem('src')->textContent;
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $imgSrc);
$_FILES = [];
}
#[Group(name: 'NonThreadSafe')]
public function testUserCannotCreateInvalidPost(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/microblog');
$crawler = $this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Add post')->form(
[
'post[body]' => '',
]
)
);
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('#content', 'This value should not be blank.');
}
public function testCreatedPostIsMarkedAsForAdults(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', hideAdult: false));
$this->getMagazineByName('acme');
$crawler = $this->client->request('GET', '/m/acme/microblog');
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Add post')->form(
[
'post[body]' => 'test nsfw 1',
'post[isAdult]' => '1',
]
)
);
$this->assertResponseRedirects('/m/acme/microblog/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('blockquote header .danger', '18+');
}
#[Group(name: 'NonThreadSafe')]
public function testPostCreatedInAdultMagazineIsAutomaticallyMarkedAsForAdults(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe', hideAdult: false));
$this->getMagazineByName('adult', isAdult: true);
$crawler = $this->client->request('GET', '/m/adult/microblog');
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Add post')->form(
[
'post[body]' => 'test nsfw 1',
]
)
);
$this->assertResponseRedirects('/m/adult/microblog/newest');
$this->client->followRedirect();
$this->assertSelectorTextContains('blockquote header .danger', '18+');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostDeleteControllerTest.php
================================================
getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$post = $this->createPost('deletion test', magazine: $magazine, user: $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
$this->assertSelectorExists('form[action$="delete"]');
$this->client->submit(
$crawler->filter('form[action$="delete"]')->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
}
public function testUserCanSoftDeletePost()
{
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('acme');
$post = $this->createPost('deletion test', magazine: $magazine, user: $user);
$comment = $this->createPostComment('really?', $post, $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
$this->assertSelectorExists("#post-{$post->getId()} form[action$=\"delete\"]");
$this->client->submit(
$crawler->filter("#post-{$post->getId()} form[action$=\"delete\"]")->selectButton('Delete')->form()
);
$this->assertResponseRedirects();
$this->client->request('GET', "/m/acme/p/{$post->getId()}/deletion-test");
$translator = $this->translator;
$this->assertSelectorTextContains("#post-{$post->getId()} .content", $translator->trans('deleted_by_author'));
}
}
================================================
FILE: tests/Functional/Controller/Post/PostEditControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .post');
$this->assertSelectorTextContains('#post_body', 'test post 1');
// $this->assertEquals('disabled', $crawler->filter('#post_magazine_autocomplete')->attr('disabled')); @todo
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Edit post')->form(
[
'post[body]' => 'test post 2 body',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .post .content', 'test post 2 body');
}
#[Group(name: 'NonThreadSafe')]
public function testAuthorCanEditOwnPostWithImage(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$imageDto = $this->getKibbyImageDto();
$post = $this->createPost('test post 1', imageDto: $imageDto);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .post');
$this->assertSelectorTextContains('#post_body', 'test post 1');
// $this->assertEquals('disabled', $crawler->filter('#post_magazine_autocomplete')->attr('disabled')); @todo
$this->assertSelectorExists('#main .post img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Edit post')->form(
[
'post[body]' => 'test post 2 body',
]
)
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#main .post .content', 'test post 2 body');
$this->assertSelectorExists('#main .post img');
$node = $crawler->selectImage('kibby')->getNode(0);
$this->assertNotNull($node);
$this->assertStringContainsString($imageDto->filePath, $node->attributes->getNamedItem('src')->textContent);
}
public function testAuthorCanEditPostToMarkItIsForAdults(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1/edit");
$crawler = $this->client->click($crawler->filter('#main .post')->selectLink('Edit')->link());
$this->assertSelectorExists('#main .post');
$this->client->submit(
$crawler->filter('form[name=post]')->selectButton('Edit post')->form(
[
'post[isAdult]' => '1',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('blockquote header .danger', '18+');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostFrontControllerTest.php
================================================
client = $this->prepareEntries();
$this->client->request('GET', '/microblog');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/microblog/newest');
$this->assertSelectorTextContains('.post header', 'JohnDoe');
$this->assertSelectorTextContains('.post header', 'to acme');
$this->assertSelectorTextContains('#header .active', 'Microblog');
$this->assertcount(2, $crawler->filter('.post'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testMagazinePage(): void
{
$this->client = $this->prepareEntries();
$this->client->request('GET', '/m/acme/microblog');
$this->assertSelectorTextContains('h2', 'Hot');
$crawler = $this->client->request('GET', '/m/acme/microblog/newest');
$this->assertSelectorTextContains('.post header', 'JohnDoe');
$this->assertSelectorTextNotContains('.post header', 'to acme');
$this->assertSelectorTextContains('.head-title', '/m/acme');
$this->assertSelectorTextContains('#sidebar .magazine', 'acme');
$this->assertSelectorTextContains('#header .active', 'Microblog');
$this->assertcount(1, $crawler->filter('.post'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', 'acme');
$this->assertSelectorTextContains('h2', ucfirst($sortOption));
}
}
public function testSubPage(): void
{
$this->client = $this->prepareEntries();
$magazineManager = $this->magazineManager;
$magazineManager->subscribe($this->getMagazineByName('acme'), $this->getUserByUsername('Actor'));
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/sub/microblog');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/sub/microblog/newest');
$this->assertSelectorTextContains('.post header', 'JohnDoe');
$this->assertSelectorTextContains('.post header', 'to acme');
$this->assertSelectorTextContains('.head-title', '/sub');
$this->assertSelectorTextContains('#header .active', 'Microblog');
$this->assertcount(1, $crawler->filter('.post'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testModPage(): void
{
$this->client = $this->prepareEntries();
$admin = $this->getUserByUsername('admin', isAdmin: true);
$magazineManager = $this->client->getContainer()->get(MagazineManager::class);
$moderator = new ModeratorDto($this->getMagazineByName('acme'));
$moderator->user = $this->getUserByUsername('Actor');
$moderator->addedBy = $admin;
$magazineManager->addModerator($moderator);
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/mod/microblog');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/mod/microblog/newest');
$this->assertSelectorTextContains('.post header', 'JohnDoe');
$this->assertSelectorTextContains('.post header', 'to acme');
$this->assertSelectorTextContains('.head-title', '/mod');
$this->assertSelectorTextContains('#header .active', 'Microblog');
$this->assertcount(1, $crawler->filter('.post'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testFavPage(): void
{
$this->client = $this->prepareEntries();
$favouriteManager = $this->favouriteManager;
$favouriteManager->toggle($this->getUserByUsername('Actor'), $this->createPost('test post 3'));
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->client->request('GET', '/fav/microblog');
$this->assertSelectorTextContains('h1', 'Hot');
$crawler = $this->client->request('GET', '/fav/microblog/newest');
$this->assertSelectorTextContains('.post header', 'JohnDoe');
$this->assertSelectorTextContains('.post header', 'to acme');
$this->assertSelectorTextContains('.head-title', '/fav');
$this->assertSelectorTextContains('#header .active', 'Microblog');
$this->assertcount(1, $crawler->filter('.post'));
foreach ($this->getSortOptions() as $sortOption) {
$crawler = $this->client->click($crawler->filter('.options__filter')->selectLink($sortOption)->link());
$this->assertSelectorTextContains('.options__filter', $sortOption);
$this->assertSelectorTextContains('h1', ucfirst($sortOption));
}
}
public function testCustomDefaultSort(): void
{
$older = $this->createPost('Older entry');
$older->createdAt = new \DateTimeImmutable('now - 1 day');
$older->updateRanking();
$this->entityManager->flush();
$newer = $this->createPost('Newer entry');
$comment = $this->createPostComment('someone was here', post: $older);
self::assertGreaterThan($older->getRanking(), $newer->getRanking());
$user = $this->getUserByUsername('user');
$user->frontDefaultSort = ESortOptions::Newest->value;
$this->entityManager->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/microblog');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('.options__filter button', $this->translator->trans(ESortOptions::Newest->value));
$iterator = $crawler->filter('#content div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current()->firstElementChild;
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-{$newer->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current()->firstElementChild;
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-{$older->getId()}", $secondId);
$user->frontDefaultSort = ESortOptions::Commented->value;
$this->entityManager->flush();
$crawler = $this->client->request('GET', '/microblog');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('.options__filter button', $this->translator->trans(ESortOptions::Commented->value));
$children = $crawler->filter('#content div')->children();
$iterator = $children->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current()->firstElementChild;
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-{$older->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current()->firstElementChild;
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-{$newer->getId()}", $secondId);
}
private function prepareEntries(): KernelBrowser
{
$this->createPost(
'test post 1',
$this->getMagazineByName('kbin', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
// sleep so the creation time is actually 1 second apart for the sort to reliably be the same
sleep(1);
$this->createPost('test post 2');
return $this->client;
}
private function getSortOptions(): array
{
return ['Top', 'Hot', 'Newest', 'Active', 'Commented'];
}
}
================================================
FILE: tests/Functional/Controller/Post/PostLockControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1', $this->getMagazineByName('acme'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Lock')->form([]));
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#main .post footer span .fa-lock');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .post footer span .fa-lock');
}
public function testAuthorCanLockEntry(): void
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$post = $this->createPost('test post 1', $this->getMagazineByName('acme'), user: $user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}");
$this->assertSelectorExists('#main .post footer .dropdown .fa-lock');
$this->client->submit($crawler->filter('#main .post footer .dropdown')->selectButton('Lock')->form([]));
$this->client->followRedirect();
$this->assertSelectorExists('#main .post footer span .fa-lock');
}
public function testModCanUnlockPost(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$post = $this->createPost('test post 1', $this->getMagazineByName('acme'));
$this->postManager->toggleLock($post, $user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$this->assertSelectorExists('#main .post footer span .fa-lock');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .post footer span .fa-lock');
}
public function testAuthorCanUnlockEntry(): void
{
$user = $this->getUserByUsername('user');
$this->client->loginUser($user);
$post = $this->createPost('test post 1', $this->getMagazineByName('acme'), user: $user);
$this->postManager->toggleLock($post, $user);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}");
$this->assertSelectorExists('#main .post footer .dropdown .fa-lock-open');
$this->client->submit($crawler->filter('#main .post footer .dropdown')->selectButton('Unlock')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .post footer span .fa-lock');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostModerateControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('get', '/microblog');
$this->client->click($crawler->filter('#post-'.$post->getId())->selectLink('Moderate')->link());
$this->assertSelectorTextContains('.moderate-panel', 'ban');
}
public function testXmlModCanShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$crawler = $this->client->request('get', '/microblog');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('#post-'.$post->getId())->selectLink('Moderate')->link());
$this->assertStringContainsString('moderate-panel', $this->client->getResponse()->getContent());
}
public function testUnauthorizedCanNotShowPanel(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$post = $this->createPost('test post 1');
$this->client->request('get', "/m/{$post->magazine->name}/p/{$post->getId()}");
$this->assertSelectorTextNotContains('#post-'.$post->getId(), 'Moderate');
$this->client->request(
'get',
"/m/{$post->magazine->name}/p/{$post->getId()}/-/moderate"
);
$this->assertResponseStatusCodeSame(403);
}
}
================================================
FILE: tests/Functional/Controller/Post/PostPinControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost(
'test post 1',
$this->getMagazineByName('acme'),
);
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/-/moderate");
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Pin')->form([]));
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#main .post .fa-thumbtack');
$this->client->submit($crawler->filter('#main .moderate-panel')->selectButton('Unpin')->form([]));
$this->client->followRedirect();
$this->assertSelectorNotExists('#main .post .fa-thumbtack');
}
}
================================================
FILE: tests/Functional/Controller/Post/PostSingleControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$this->createPost('test post 1');
$crawler = $this->client->request('GET', '/microblog');
$this->client->click($crawler->filter('.link-muted')->link());
$this->assertSelectorTextContains('blockquote', 'test post 1');
$this->assertSelectorTextContains('#main', 'No comments');
$this->assertSelectorTextContains('#sidebar .magazine', 'Magazine');
$this->assertSelectorTextContains('#sidebar .user-list', 'Moderators');
$this->assertSelectorTextContains('.head-nav__menu .active', $this->translator->trans('microblog'));
}
public function testUserCanSeePost(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$this->assertSelectorTextContains('blockquote', 'test post 1');
$this->assertSelectorTextContains('.head-nav__menu .active', $this->translator->trans('microblog'));
}
public function testPostActivityCounter(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$manager = $this->client->getContainer()->get(VoteManager::class);
$manager->vote(VotableInterface::VOTE_DOWN, $post, $this->getUserByUsername('JaneDoe'));
$manager = $this->client->getContainer()->get(FavouriteManager::class);
$manager->toggle($this->getUserByUsername('JohnDoe'), $post);
$manager->toggle($this->getUserByUsername('JaneDoe'), $post);
$this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$this->assertSelectorTextContains('.options-activity', 'Activity (2)');
}
public function testCommentsDefaultSortOption(): void
{
$user = $this->getUserByUsername('user');
$post = $this->createPost('entry');
$older = $this->createPostComment('older comment', post: $post);
$older->createdAt = new \DateTimeImmutable('now - 1 day');
$newer = $this->createPostComment('newer comment', post: $post);
$user->commentDefaultSort = ESortOptions::Oldest->value;
$this->entityManager->flush();
$this->client->loginUser($user);
$crawler = $this->client->request('GET', "/m/{$post->magazine->name}/p/{$post->getId()}/-");
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('.options__main .active', $this->translator->trans(ESortOptions::Oldest->value));
$iterator = $crawler->filter('#comments div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-comment-{$older->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-comment-{$newer->getId()}", $secondId);
$user->commentDefaultSort = ESortOptions::Newest->value;
$this->entityManager->flush();
$crawler = $this->client->request('GET', "/m/{$post->magazine->name}/p/{$post->getId()}/-");
self::assertResponseIsSuccessful();
$this->assertSelectorTextContains('.options__main .active', $this->translator->trans(ESortOptions::Newest->value));
$iterator = $crawler->filter('#comments div')->children()->getIterator();
/** @var \DOMElement $firstNode */
$firstNode = $iterator->current();
$firstId = $firstNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-comment-{$newer->getId()}", $firstId);
$iterator->next();
$secondNode = $iterator->current();
$secondId = $secondNode->attributes->getNamedItem('id')->nodeValue;
self::assertEquals("post-comment-{$older->getId()}", $secondId);
}
}
================================================
FILE: tests/Functional/Controller/Post/PostVotersControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1');
$manager = $this->client->getContainer()->get(VoteManager::class);
$manager->vote(VotableInterface::VOTE_UP, $post, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$this->client->click($crawler->filter('.options-activity')->selectLink('Boosts (1)')->link());
$this->assertSelectorTextContains('#main .users-columns', 'JaneDoe');
}
}
================================================
FILE: tests/Functional/Controller/PrivacyPolicyControllerTest.php
================================================
client->request('GET', '/');
$this->client->click($crawler->filter('.about.section a[href="/privacy-policy"]')->link());
$this->assertSelectorTextContains('h1', 'Privacy policy');
}
}
================================================
FILE: tests/Functional/Controller/ReportControllerControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
null,
$this->getUserByUsername('JaneDoe')
);
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry menu')->selectLink('Report')->link());
$this->assertSelectorExists('#main .entry');
$this->client->submit(
$crawler->filter('form[name=report]')->selectButton('Report')->form(
[
'report[reason]' => 'test reason 1',
]
)
);
$repo = $this->reportRepository;
$this->assertEquals(1, $repo->count([]));
}
public function testLoggedUserCanReportEntryComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$entry = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
null,
$this->getUserByUsername('JaneDoe')
);
$this->createEntryComment('test comment 1', $entry, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/t/{$entry->getId()}/test-entry-1");
$crawler = $this->client->click($crawler->filter('#main .entry-comment')->selectLink('Report')->link());
$this->assertSelectorExists('#main .entry-comment');
$this->client->submit(
$crawler->filter('form[name=report]')->selectButton('Report')->form(
[
'report[reason]' => 'test reason 1',
]
)
);
$repo = $this->reportRepository;
$this->assertEquals(1, $repo->count([]));
}
public function testLoggedUserCanReportPost(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1', null, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post menu')->selectLink('Report')->link());
$this->assertSelectorExists('#main .post');
$this->client->submit(
$crawler->filter('form[name=report]')->selectButton('Report')->form(
[
'report[reason]' => 'test reason 1',
]
)
);
$repo = $this->reportRepository;
$this->assertEquals(1, $repo->count([]));
}
public function testLoggedUserCanReportPostComment(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$post = $this->createPost('test post 1', null, $this->getUserByUsername('JaneDoe'));
$this->createPostComment('test comment 1', $post, $this->getUserByUsername('JaneDoe'));
$crawler = $this->client->request('GET', "/m/acme/p/{$post->getId()}/test-post-1");
$crawler = $this->client->click($crawler->filter('#main .post-comment menu')->selectLink('Report')->link());
$this->assertSelectorExists('#main .post-comment');
$this->client->submit(
$crawler->filter('form[name=report]')->selectButton('Report')->form(
[
'report[reason]' => 'test reason 1',
]
)
);
$repo = $this->reportRepository;
$this->assertEquals(1, $repo->count([]));
}
}
================================================
FILE: tests/Functional/Controller/Security/LoginControllerTest.php
================================================
client = $this->register(true);
$crawler = $this->client->request('get', '/');
$crawler = $this->client->click($crawler->filter('header')->selectLink('Log in')->link());
$this->client->submit(
$crawler->selectButton('Log in')->form(
[
'email' => 'JohnDoe',
'password' => 'secret',
]
)
);
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains('#header', 'JohnDoe');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCannotLoginWithoutActivation(): void
{
$this->client = $this->register();
$crawler = $this->client->request('get', '/');
$crawler = $this->client->click($crawler->filter('header')->selectLink('Log in')->link());
$this->client->submit(
$crawler->selectButton('Log in')->form(
[
'email' => 'JohnDoe',
'password' => 'secret',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main', 'Please check your email for account activation instructions or request a new account activation email');
}
public function testUserCantLoginWithWrongPassword(): void
{
$this->getUserByUsername('JohnDoe');
$crawler = $this->client->request('GET', '/');
$crawler = $this->client->click($crawler->filter('header')->selectLink('Log in')->link());
$this->client->submit(
$crawler->selectButton('Log in')->form(
[
'email' => 'JohnDoe',
'password' => 'wrongpassword',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('.alert__danger', 'Invalid credentials.'); // @todo
}
}
================================================
FILE: tests/Functional/Controller/Security/OAuth2ConsentControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
self::runAuthorizationCodeFlowToConsentPage($this->client, 'read write', 'oauth2state');
self::assertSelectorTextContains("li[id='oauth2.grant.read.general']", 'Read all content you have access to.');
self::assertSelectorTextContains("li[id='oauth2.grant.write.general']", 'Create or edit any of your threads, posts, or comments.');
self::runAuthorizationCodeFlowToRedirectUri($this->client, 'read write', 'yes', 'oauth2state');
$response = $this->client->getResponse();
$parsedUrl = parse_url($response->headers->get('Location'));
self::assertEquals('https', $parsedUrl['scheme']);
self::assertEquals('localhost', $parsedUrl['host']);
self::assertEquals('3001', $parsedUrl['port']);
self::assertStringContainsString('code', $parsedUrl['query']);
}
public function testUserCanDissent(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
self::runAuthorizationCodeFlowToConsentPage($this->client, 'read write', 'oauth2state');
self::assertSelectorTextContains("li[id='oauth2.grant.read.general']", 'Read all content you have access to.');
self::assertSelectorTextContains("li[id='oauth2.grant.write.general']", 'Create or edit any of your threads, posts, or comments.');
self::runAuthorizationCodeFlowToRedirectUri($this->client, 'read write', 'no', 'oauth2state');
$response = $this->client->getResponse();
$parsedUrl = parse_url($response->headers->get('Location'));
self::assertEquals('https', $parsedUrl['scheme']);
self::assertEquals('localhost', $parsedUrl['host']);
self::assertEquals('3001', $parsedUrl['port']);
self::assertStringContainsString('error=access_denied', $parsedUrl['query']);
}
}
================================================
FILE: tests/Functional/Controller/Security/OAuth2TokenControllerTest.php
================================================
client->request('POST', '/token', [
'grant_type' => 'client_credentials',
'client_id' => 'testclient',
'client_secret' => 'testsecret',
'scope' => 'read write',
]);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('token_type', $jsonData);
self::assertEquals('Bearer', $jsonData['token_type']);
self::assertArrayHasKey('expires_in', $jsonData);
self::assertIsInt($jsonData['expires_in']);
self::assertArrayHasKey('access_token', $jsonData);
self::assertMatchesRegularExpression(self::JWT_REGEX, $jsonData['access_token']);
self::assertArrayNotHasKey('refresh_token', $jsonData);
}
public function testCanGetTokenWithValidAuthorizationCode(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('token_type', $jsonData);
self::assertEquals('Bearer', $jsonData['token_type']);
self::assertArrayHasKey('expires_in', $jsonData);
self::assertIsInt($jsonData['expires_in']);
self::assertArrayHasKey('access_token', $jsonData);
self::assertMatchesRegularExpression(self::JWT_REGEX, $jsonData['access_token']);
self::assertArrayHasKey('refresh_token', $jsonData);
self::assertMatchesRegularExpression(self::CODE_REGEX, $jsonData['refresh_token']);
}
public function testCanGetTokenWithValidRefreshToken(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('refresh_token', $jsonData);
$jsonData = self::getRefreshTokenResponse($this->client, $jsonData['refresh_token']);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('token_type', $jsonData);
self::assertEquals('Bearer', $jsonData['token_type']);
self::assertArrayHasKey('expires_in', $jsonData);
self::assertIsInt($jsonData['expires_in']);
self::assertArrayHasKey('access_token', $jsonData);
self::assertMatchesRegularExpression(self::JWT_REGEX, $jsonData['access_token']);
self::assertArrayHasKey('refresh_token', $jsonData);
self::assertMatchesRegularExpression(self::CODE_REGEX, $jsonData['refresh_token']);
}
public function testCanGetTokenWithValidAuthorizationCodePKCE(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2PublicAuthCodeClient();
$jsonData = self::getPublicAuthorizationCodeTokenResponse($this->client);
self::assertResponseIsSuccessful();
self::assertIsArray($jsonData);
self::assertArrayHasKey('token_type', $jsonData);
self::assertEquals('Bearer', $jsonData['token_type']);
self::assertArrayHasKey('expires_in', $jsonData);
self::assertIsInt($jsonData['expires_in']);
self::assertArrayHasKey('access_token', $jsonData);
self::assertMatchesRegularExpression(self::JWT_REGEX, $jsonData['access_token']);
self::assertArrayHasKey('refresh_token', $jsonData);
self::assertMatchesRegularExpression(self::CODE_REGEX, $jsonData['refresh_token']);
}
public function testCannotGetTokenWithInvalidVerifierAuthorizationCodePKCE(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2PublicAuthCodeClient();
$pkceCodes = self::runPublicAuthorizationCodeFlow($this->client, 'yes');
self::runPublicAuthorizationCodeTokenFetch($this->client, $pkceCodes['verifier'].'fail');
$jsonData = self::getJsonResponse($this->client);
self::assertResponseStatusCodeSame(400);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_grant', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
self::assertArrayHasKey('hint', $jsonData);
}
public function testCannotGetTokenWithoutChallengeAuthorizationCodePKCE(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2PublicAuthCodeClient();
$query = self::buildPrivateAuthCodeQuery('testpublicclient', 'read write', 'oauth2state', 'https://localhost:3001');
$uri = '/authorize?'.$query;
$this->client->request('GET', $uri);
$jsonData = self::getJsonResponse($this->client);
self::assertResponseStatusCodeSame(400);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_request', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
self::assertArrayHasKey('hint', $jsonData);
self::assertStringContainsStringIgnoringCase('code challenge', $jsonData['hint']);
}
public function testReceiveErrorWithInvalidAuthorizationCode(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$this->client->request('POST', '/token', [
'grant_type' => 'authorization_code',
'client_id' => 'testclient',
'client_secret' => 'testsecret',
'code' => 'deadbeefc0de',
'redirect_uri' => 'https://localhost:3001',
]);
self::assertResponseStatusCodeSame(400);
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_grant', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
self::assertArrayHasKey('hint', $jsonData);
}
public function testReceiveErrorWithInvalidClientId(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$query = self::buildPrivateAuthCodeQuery('testclientfake', 'read write', 'oauth2state', 'https://localhost:3001');
$uri = '/authorize?'.$query;
$this->client->request('GET', $uri);
$jsonData = self::getJsonResponse($this->client);
self::assertResponseStatusCodeSame(401);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_client', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
}
public function testReceiveErrorWithInvalidClientSecret(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$jsonData = self::getAuthorizationCodeTokenResponse($this->client, clientSecret: 'testsecretfake');
self::assertResponseStatusCodeSame(401);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_client', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
}
public function testReceiveErrorWithInvalidRedirectUri(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
self::createOAuth2AuthCodeClient();
$query = self::buildPrivateAuthCodeQuery('testclient', 'read write', 'oauth2state', 'https://invalid.com');
$uri = '/authorize?'.$query;
$this->client->request('GET', $uri);
$jsonData = self::getJsonResponse($this->client);
self::assertResponseStatusCodeSame(401);
self::assertIsArray($jsonData);
self::assertArrayHasKey('error', $jsonData);
self::assertEquals('invalid_client', $jsonData['error']);
self::assertArrayHasKey('error_description', $jsonData);
}
}
================================================
FILE: tests/Functional/Controller/Security/RegisterControllerTest.php
================================================
registerUserAccount($this->client);
$this->assertEmailCount(1);
/** @var TemplatedEmail $email */
$email = $this->getMailerMessage();
$this->assertEmailHeaderSame($email, 'To', 'johndoe@kbin.pub');
$verificationLink = (new Crawler($email->getHtmlBody()))
->filter('a.btn.btn__primary')
->attr('href')
;
$this->client->request('GET', $verificationLink);
$crawler = $this->client->followRedirect();
$this->client->submit(
$crawler->selectButton('Log in')->form(
[
'email' => 'JohnDoe',
'password' => 'secret',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextNotContains('#header', 'Log in');
}
private function registerUserAccount(KernelBrowser $client): void
{
$crawler = $client->request('GET', '/register');
$client->submit(
$crawler->filter('form[name=user_register]')->selectButton('Register')->form(
[
'user_register[username]' => 'JohnDoe',
'user_register[email]' => 'johndoe@kbin.pub',
'user_register[plainPassword][first]' => 'secret',
'user_register[plainPassword][second]' => 'secret',
'user_register[agreeTerms]' => true,
]
)
);
}
public function testUserCannotLoginWithoutConfirmation()
{
$this->registerUserAccount($this->client);
$crawler = $this->client->followRedirect();
$crawler = $this->client->click($crawler->filter('#header')->selectLink('Log in')->link());
$this->client->submit(
$crawler->selectButton('Log in')->form(
[
'email' => 'JohnDoe',
'password' => 'wrong_password',
]
)
);
$this->client->followRedirect();
$this->assertSelectorTextContains('.alert__danger', 'Your account has not been activated.');
}
}
================================================
FILE: tests/Functional/Controller/TermsControllerTest.php
================================================
client->request('GET', '/');
$this->client->click($crawler->filter('.about.section a[href="/terms"]')->link());
$this->assertSelectorTextContains('h1', 'Terms');
}
}
================================================
FILE: tests/Functional/Controller/User/Admin/UserDeleteControllerTest.php
================================================
getUserByUsername('user');
$entry = $this->getEntryByTitle('An entry', body: 'test', user: $user);
$entryComment = $this->createEntryComment('A comment', $entry, $user);
$post = $this->createPost('A post', user: $user);
$postComment = $this->createPostComment('A comment', $post, $user);
$admin = $this->getUserByUsername('admin', isAdmin: true);
$this->client->loginUser($admin);
$crawler = $this->client->request('GET', '/u/user');
$this->assertSelectorExists('#sidebar .panel form[action$="delete_account"]');
$this->client->submit(
$crawler->filter('#sidebar .panel form[action$="delete_account"]')->selectButton('Delete account')->form()
);
$this->assertResponseRedirects();
}
}
================================================
FILE: tests/Functional/Controller/User/Profile/UserBlockControllerTest.php
================================================
client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('acme');
$this->magazineManager->block($magazine, $user);
$crawler = $this->client->request('GET', '/settings/blocked/magazines');
$this->client->click($crawler->filter('#main .pills')->selectLink('Magazines')->link());
$this->assertSelectorTextContains('#main .pills .active', 'Magazines');
$this->assertSelectorTextContains('#main .magazines', 'acme');
}
public function testUserCanSeeBlockedUsers()
{
$this->client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$this->userManager->block($user, $this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/blocked/people');
$this->client->click($crawler->filter('#main .pills')->selectLink('People')->link());
$this->assertSelectorTextContains('#main .pills .active', 'People');
$this->assertSelectorTextContains('#main .users', 'JohnDoe');
}
public function testUserCanSeeBlockedDomains()
{
$this->client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test1', 'https://kbin.pub');
$this->domainManager->block($entry->domain, $user);
$crawler = $this->client->request('GET', '/settings/blocked/domains');
$this->client->click($crawler->filter('#main .pills')->selectLink('Domains')->link());
$this->assertSelectorTextContains('#main .pills .active', 'Domains');
$this->assertSelectorTextContains('#main', 'kbin.pub');
}
}
================================================
FILE: tests/Functional/Controller/User/Profile/UserEditControllerTest.php
================================================
kibbyPath = \dirname(__FILE__, 5).'/assets/kibby_emoji.png';
}
public function testUserCanSeeSettingsLink(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/');
$this->client->click($crawler->filter('#header menu')->selectLink('Settings')->link());
$this->assertSelectorTextContains('#main .options__main a.active', 'General');
}
public function testUserCanEditProfileAbout(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/profile');
$this->assertSelectorTextContains('#main .options__main a.active', 'Profile');
$this->client->submit(
$crawler->filter('#main form[name=user_basic]')->selectButton('Save')->form([
'user_basic[about]' => 'test about',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .user-box', 'test about');
$this->client->request('GET', '/people');
$this->assertSelectorTextContains('#main .user-box', 'JohnDoe');
}
public function testUserCanEditProfileTitle(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/profile');
$this->assertSelectorTextContains('#main .options__main a.active', 'Profile');
$this->client->submit(
$crawler->filter('#main form[name=user_basic]')->selectButton('Save')->form([
'user_basic[title]' => 'custom name',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .user-box h1', 'custom name');
$this->client->request('GET', '/people');
$this->assertSelectorTextContains('#main .user-box', 'JohnDoe');
}
public function testUserEditProfileTitleTrimsWhitespace(): void
{
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/profile');
$this->assertSelectorTextContains('#main .options__main a.active', 'Profile');
$this->client->submit(
$crawler->filter('#main form[name=user_basic]')->selectButton('Save')->form([
'user_basic[title]' => " custom name\t",
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#main .user-box h1', 'custom name');
$this->client->request('GET', '/people');
$this->assertSelectorTextContains('#main .user-box', 'JohnDoe');
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanUploadAvatar(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$repository = $this->userRepository;
$crawler = $this->client->request('GET', '/settings/profile');
$this->assertSelectorTextContains('#main .options__main a.active', 'Profile');
$this->assertStringContainsString('/dev/random', $user->avatar->filePath);
$form = $crawler->filter('#main form[name=user_basic]')->selectButton('Save')->form();
$form['user_basic[avatar]']->upload($this->kibbyPath);
$this->client->submit($form);
$user = $repository->find($user->getId());
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $user->avatar->filePath);
}
#[Group(name: 'NonThreadSafe')]
public function testUserCanUploadCover(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->loginUser($user);
$repository = $this->userRepository;
$crawler = $this->client->request('GET', '/settings/profile');
$this->assertSelectorTextContains('#main .options__main a.active', 'Profile');
$this->assertNull($user->cover);
$form = $crawler->filter('#main form[name=user_basic]')->selectButton('Save')->form();
$form['user_basic[cover]']->upload($this->kibbyPath);
$this->client->submit($form);
$user = $repository->find($user->getId());
$this->assertStringContainsString(self::KIBBY_PNG_URL_RESULT, $user->cover->filePath);
}
public function testUserCanChangePassword(): void
{
$this->client = $this->register(true);
$this->client->loginUser($this->userRepository->findOneBy(['username' => 'JohnDoe']));
$crawler = $this->client->request('GET', '/settings/password');
$this->assertSelectorTextContains('#main .options__main a.active', 'Password');
$this->client->submit(
$crawler->filter('#main form[name=user_password]')->selectButton('Save')->form([
'user_password[currentPassword]' => 'secret',
'user_password[plainPassword][first]' => 'test123',
'user_password[plainPassword][second]' => 'test123',
])
);
$this->client->followRedirect();
$crawler = $this->client->request('GET', '/login');
$this->client->submit(
$crawler->filter('#main form')->selectButton('Log in')->form([
'email' => 'JohnDoe',
'password' => 'test123',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#header', 'JohnDoe');
}
public function testUserCanChangeEmail(): void
{
$this->client = $this->register(true);
$this->client->loginUser($this->userRepository->findOneBy(['username' => 'JohnDoe']));
$crawler = $this->client->request('GET', '/settings/email');
$this->assertSelectorTextContains('#main .options__main a.active', 'Email');
$this->client->submit(
$crawler->filter('#main form[name=user_email]')->selectButton('Save')->form([
'user_email[newEmail][first]' => 'acme@kbin.pub',
'user_email[newEmail][second]' => 'acme@kbin.pub',
'user_email[currentPassword]' => 'secret',
])
);
$this->assertEmailCount(1);
/** @var TemplatedEmail $email */
$email = $this->getMailerMessage();
$this->assertEmailHeaderSame($email, 'To', 'acme@kbin.pub');
$verificationLink = (new Crawler($email->getHtmlBody()))
->filter('a.btn.btn__primary')
->attr('href')
;
$this->client->request('GET', $verificationLink);
$crawler = $this->client->followRedirect();
$this->client->submit(
$crawler->filter('#main form')->selectButton('Log in')->form([
'email' => 'JohnDoe',
'password' => 'secret',
])
);
$this->client->followRedirect();
$this->assertSelectorTextContains('#header', 'JohnDoe');
}
}
================================================
FILE: tests/Functional/Controller/User/Profile/UserNotificationControllerTest.php
================================================
client->loginUser($owner = $this->getUserByUsername('owner'));
$actor = $this->getUserByUsername('actor');
$this->magazineManager->subscribe($this->getMagazineByName('acme'), $owner);
$this->magazineManager->subscribe($this->getMagazineByName('acme'), $actor);
$this->loadNotificationsFixture();
$crawler = $this->client->request('GET', '/settings/notifications');
$this->assertCount(2, $crawler->filter('#main .notification'));
$this->client->restart();
$this->client->loginUser($actor);
$crawler = $this->client->request('GET', '/settings/notifications');
$this->assertCount(3, $crawler->filter('#main .notification'));
$this->client->restart();
$this->client->loginUser($this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/notifications');
$this->assertCount(2, $crawler->filter('#main .notification'));
}
public function testCanReadAllNotifications(): void
{
$this->client->loginUser($this->getUserByUsername('owner'));
$this->magazineManager->subscribe(
$this->getMagazineByName('acme'),
$this->getUserByUsername('owner')
);
$this->magazineManager->subscribe(
$this->getMagazineByName('acme'),
$this->getUserByUsername('actor')
);
$this->loadNotificationsFixture();
$this->client->loginUser($this->getUserByUsername('owner'));
$crawler = $this->client->request('GET', '/settings/notifications');
$this->assertCount(2, $crawler->filter('#main .notification'));
$this->assertCount(0, $crawler->filter('#main .notification.opacity-50'));
$this->client->submit($crawler->selectButton('Read all')->form());
$crawler = $this->client->followRedirect();
$this->assertCount(2, $crawler->filter('#main .notification.opacity-50'));
}
public function testUserCanDeleteAllNotifications(): void
{
$this->client->loginUser($this->getUserByUsername('owner'));
$this->magazineManager->subscribe(
$this->getMagazineByName('acme'),
$this->getUserByUsername('owner')
);
$this->magazineManager->subscribe(
$this->getMagazineByName('acme'),
$this->getUserByUsername('actor')
);
$this->loadNotificationsFixture();
$this->client->loginUser($this->getUserByUsername('owner'));
$crawler = $this->client->request('GET', '/settings/notifications');
$this->assertCount(2, $crawler->filter('#main .notification'));
$this->client->submit($crawler->selectButton('Purge')->form());
$crawler = $this->client->followRedirect();
$this->assertCount(0, $crawler->filter('#main .notification'));
}
}
================================================
FILE: tests/Functional/Controller/User/Profile/UserSubControllerTest.php
================================================
client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$magazine = $this->getMagazineByName('acme');
$this->magazineManager->subscribe($magazine, $user);
$crawler = $this->client->request('GET', '/settings/subscriptions/magazines');
$this->client->click($crawler->filter('#main .pills')->selectLink('Magazines')->link());
$this->assertSelectorTextContains('#main .pills .active', 'Magazines');
$this->assertSelectorTextContains('#main .magazines', 'acme');
}
public function testUserCanSeeSubscribedUsers()
{
$this->client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$this->userManager->follow($user, $this->getUserByUsername('JohnDoe'));
$crawler = $this->client->request('GET', '/settings/subscriptions/people');
$this->client->click($crawler->filter('#main .pills')->selectLink('People')->link());
$this->assertSelectorTextContains('#main .pills .active', 'People');
$this->assertSelectorTextContains('#main .users', 'JohnDoe');
}
public function testUserCanSeeSubscribedDomains()
{
$this->client->loginUser($user = $this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test1', 'https://kbin.pub');
$this->domainManager->subscribe($entry->domain, $user);
$crawler = $this->client->request('GET', '/settings/subscriptions/domains');
$this->client->click($crawler->filter('#main .pills')->selectLink('Domains')->link());
$this->assertSelectorTextContains('#main .pills .active', 'Domains');
$this->assertSelectorTextContains('#main', 'kbin.pub');
}
}
================================================
FILE: tests/Functional/Controller/User/UserBlockControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId().'/test-entry-1');
// Block
$this->client->submit($crawler->filter('#sidebar form[name=user_block] button')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=user_block] .active');
// Unblock
$this->client->submit($crawler->filter('#sidebar form[name=user_block] button')->form());
$this->client->followRedirect();
$this->assertSelectorNotExists('#sidebar form[name=user_block] .active');
}
public function testXmlUserCanBlock(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId().'/test-entry-1');
// Block
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=user_block] button')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('active', $this->client->getResponse()->getContent());
}
#[Group(name: 'NonThreadSafe')]
public function testXmlUserCanUnblock(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId().'/test-entry-1');
// Block
$this->client->submit($crawler->filter('#sidebar form[name=user_block] button')->form());
$crawler = $this->client->followRedirect();
// Unblock
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar form[name=user_block] button')->form());
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringNotContainsString('active', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/User/UserFollowControllerTest.php
================================================
client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId());
// Follow
$this->client->submit($crawler->filter('#sidebar .entry-info')->selectButton('Follow')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorExists('#sidebar form[name=user_follow] .active');
$this->assertSelectorTextContains('#sidebar .entry-info', 'Unfollow');
$this->assertSelectorTextContains('#sidebar .entry-info', '1');
// Unfollow
$this->client->submit($crawler->filter('#sidebar .entry-info')->selectButton('Unfollow')->form());
$this->client->followRedirect();
$this->assertSelectorNotExists('#sidebar form[name=user_follow] .active');
$this->assertSelectorTextContains('#sidebar .entry-info', 'Follow');
$this->assertSelectorTextContains('#sidebar .entry-info', '0');
}
public function testXmlUserCanFollow(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId());
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .entry-info')->selectButton('Follow')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Unfollow', $this->client->getResponse()->getContent());
}
#[Group(name: 'NonThreadSafe')]
public function testXmlUserCanUnfollow(): void
{
$this->client->loginUser($this->getUserByUsername('JaneDoe'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId());
// Follow
$this->client->submit($crawler->filter('#sidebar .entry-info')->selectButton('Follow')->form());
$crawler = $this->client->followRedirect();
// Unfollow
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->submit($crawler->filter('#sidebar .entry-info')->selectButton('Unfollow')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Follow', $this->client->getResponse()->getContent());
}
}
================================================
FILE: tests/Functional/Controller/User/UserFrontControllerTest.php
================================================
client = $this->prepareEntries();
$crawler = $this->client->request('GET', '/u/JohnDoe');
$this->assertSelectorTextContains('.options.options .active', 'Overview');
$this->assertEquals(2, $crawler->filter('#main .entry')->count());
$this->assertEquals(2, $crawler->filter('#main .entry-comment')->count());
$this->assertEquals(2, $crawler->filter('#main .post')->count());
$this->assertEquals(2, $crawler->filter('#main .post-comment')->count());
}
public function testThreadsPage(): void
{
$this->client = $this->prepareEntries();
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('Threads')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Threads (1)');
$this->assertEquals(1, $crawler->filter('#main .entry')->count());
}
public function testCommentsPage(): void
{
$this->client = $this->prepareEntries();
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$this->client->click($crawler->filter('#main .options')->selectLink('Comments')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Comments (2)');
$this->assertEquals(2, $crawler->filter('#main .entry-comment')->count());
}
public function testPostsPage(): void
{
$this->client = $this->prepareEntries();
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('Posts')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Posts (1)');
$this->assertEquals(1, $crawler->filter('#main .post')->count());
}
public function testRepliesPage(): void
{
$this->client = $this->prepareEntries();
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('Replies')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Replies (2)');
$this->assertEquals(2, $crawler->filter('#main .post-comment')->count());
$this->assertEquals(2, $crawler->filter('#main .post')->count());
}
public function createSubscriptionsPage()
{
$user = $this->getUserByUsername('JohnDoe');
$this->getMagazineByName('kbin');
$this->getMagazineByName('mag', $this->getUserByUsername('JaneDoe'));
$manager = $this->magazineManager;
$manager->subscribe($this->getMagazineByName('mag'), $user);
$this->client->loginUser($user);
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('subscriptions')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'subscriptions (2)');
$this->assertEquals(2, $crawler->filter('#main .magazines ul li')->count());
}
public function testFollowersPage(): void
{
$user1 = $this->getUserByUsername('JohnDoe');
$user2 = $this->getUserByUsername('JaneDoe');
$manager = $this->userManager;
$manager->follow($user2, $user1);
$this->client->loginUser($user1);
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('Followers')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Followers (1)');
$this->assertEquals(1, $crawler->filter('#main .users ul li')->count());
}
public function testFollowingPage(): void
{
$user1 = $this->getUserByUsername('JohnDoe');
$user2 = $this->getUserByUsername('JaneDoe');
$manager = $this->userManager;
$manager->follow($user1, $user2);
$this->client->loginUser($user1);
$crawler = $this->client->request('GET', '/u/JohnDoe');
self::assertResponseIsSuccessful();
$crawler = $this->client->click($crawler->filter('#main .options')->selectLink('Following')->link());
$this->assertSelectorTextContains('.options.options--top .active', 'Following (1)');
$this->assertEquals(1, $crawler->filter('#main .users ul li')->count());
}
public function testNewIndicator(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->request('GET', '/u/JohnDoe');
$this->assertSelectorExists('#content.user-main h1 i.fa-solid.fa-leaf.new-user-icon');
$user->createdAt = new \DateTimeImmutable('now - 31days');
$this->entityManager->flush();
$this->client->request('GET', '/u/JohnDoe');
$this->assertSelectorNotExists('#content.user-main h1 i.fa-solid.fa-leaf.new-user-icon');
}
public function testCakeDayIndicator(): void
{
$user = $this->getUserByUsername('JohnDoe');
$this->client->request('GET', '/u/JohnDoe');
$this->assertSelectorExists('#content.user-main h1 i.fa-solid.fa-cake-candles');
$user->createdAt = new \DateTimeImmutable('now - 1days');
$this->entityManager->flush();
$this->client->request('GET', '/u/JohnDoe');
$this->assertSelectorNotExists('#content.user-main h1 i.fa-solid.fa-cake-candles');
}
private function prepareEntries(): KernelBrowser
{
$entry1 = $this->getEntryByTitle(
'test entry 1',
'https://kbin.pub',
null,
$this->getMagazineByName('mag', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
$entry2 = $this->getEntryByTitle(
'test entry 2',
'https://kbin.pub',
null,
$this->getMagazineByName('mag', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
$entry3 = $this->getEntryByTitle('test entry 3', 'https://kbin.pub');
$this->createEntryComment('test entry comment 1', $entry1);
$this->createEntryComment('test entry comment 2', $entry2, $this->getUserByUsername('JaneDoe'));
$this->createEntryComment('test entry comment 3', $entry3);
$post1 = $this->createPost(
'test post 1',
$this->getMagazineByName('mag', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
$post2 = $this->createPost(
'test post 2',
$this->getMagazineByName('mag', $this->getUserByUsername('JaneDoe')),
$this->getUserByUsername('JaneDoe')
);
$post3 = $this->createPost('test post 3');
$this->createPostComment('test post comment 1', $post1);
$this->createPostComment('test post comment 2', $post2, $this->getUserByUsername('JaneDoe'));
$this->createPostComment('test post comment 3', $post3);
return $this->client;
}
}
================================================
FILE: tests/Functional/Controller/VoteControllerTest.php
================================================
client->loginUser($this->getUserByUsername('Actor'));
$entry = $this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$u1 = $this->getUserByUsername('JohnDoe');
$u2 = $this->getUserByUsername('JaneDoe');
$this->createVote(1, $entry, $u1);
$this->createVote(1, $entry, $u2);
$this->client->request('GET', '/');
$crawler = $this->client->request('GET', '/m/acme/t/'.$entry->getId().'/-/comments');
$this->assertUpDownVoteActions($crawler);
}
public function testXmlUserCanVoteOnEntry(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->getEntryByTitle('test entry 1', 'https://kbin.pub');
$crawler = $this->client->request('GET', '/');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('.entry .vote__up')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testUserCanVoteOnEntryComment(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$comment = $this->createEntryComment('test entry comment 1');
$u1 = $this->getUserByUsername('JohnDoe');
$u2 = $this->getUserByUsername('JaneDoe');
$this->createVote(1, $comment, $u1);
$this->createVote(1, $comment, $u2);
$this->client->request('GET', '/');
$crawler = $this->client->request('GET', '/m/acme/t/'.$comment->entry->getId().'/-/comments');
$this->assertUpDownVoteActions($crawler, '.comment');
}
public function testXmlUserCanVoteOnEntryComment(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$comment = $this->createEntryComment('test entry comment 1');
$crawler = $this->client->request('GET', '/m/acme/t/'.$comment->entry->getId().'/-/comments');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('.entry-comment .vote__up')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
private function assertUpDownVoteActions($crawler, string $selector = ''): void
{
$this->assertSelectorTextContains($selector.' .vote__up', '2');
$this->assertSelectorTextContains($selector.' .vote__down', '0');
$this->client->click($crawler->filter($selector.' .vote__up')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '3');
$this->assertSelectorTextContains($selector.' .vote__down', '0');
$this->client->click($crawler->filter($selector.' .vote__down')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '2');
$this->assertSelectorTextContains($selector.' .vote__down', '1');
$this->client->click($crawler->filter($selector.' .vote__down')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '2');
$this->assertSelectorTextContains($selector.' .vote__down', '0');
$this->client->submit($crawler->filter($selector.' .vote__up')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '3');
$this->assertSelectorTextContains($selector.' .vote__down', '0');
$this->client->submit($crawler->filter($selector.' .vote__up')->form());
$this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '2');
$this->assertSelectorTextContains($selector.' .vote__down', '0');
}
public function testUserCanVoteOnPost(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$post = $this->createPost('test post 1');
$u1 = $this->getUserByUsername('JohnDoe');
$u2 = $this->getUserByUsername('JaneDoe');
$this->createVote(1, $post, $u1);
$this->createVote(1, $post, $u2);
$crawler = $this->client->request('GET', '/m/acme/p/'.$post->getId().'/-');
self::assertResponseIsSuccessful();
$this->assertUpVoteActions($crawler);
}
public function testXmlUserCanVoteOnPost(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$this->createPost('test post 1');
$crawler = $this->client->request('GET', '/microblog');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('.post .vote__up')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
public function testUserCanVoteOnPostComment(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$comment = $this->createPostComment('test post comment 1');
$u1 = $this->getUserByUsername('JohnDoe');
$u2 = $this->getUserByUsername('JaneDoe');
$this->createVote(1, $comment, $u1);
$this->createVote(1, $comment, $u2);
$crawler = $this->client->request('GET', '/m/acme/p/'.$comment->post->getId());
$this->assertUpVoteActions($crawler, '.comment');
}
public function testXmlUserCanVoteOnPostComment(): void
{
$this->client->loginUser($this->getUserByUsername('Actor'));
$comment = $this->createPostComment('test post comment 1');
$crawler = $this->client->request('GET', '/m/acme/p/'.$comment->post->getId().'/-');
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->click($crawler->filter('.post-comment .vote__up')->form());
$this->assertStringContainsString('{"html":', $this->client->getResponse()->getContent());
}
private function assertUpVoteActions($crawler, string $selector = ''): void
{
$this->assertSelectorTextContains($selector.' .vote__up', '2');
$this->client->submit($crawler->filter($selector.' .vote__up')->form());
$crawler = $this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '3');
$this->client->submit($crawler->filter($selector.' .vote__up')->form());
$this->client->followRedirect();
$this->assertSelectorTextContains($selector.' .vote__up', '2');
}
}
================================================
FILE: tests/Functional/Controller/WebfingerControllerTest.php
================================================
settingsManager->get('KBIN_DOMAIN');
$resource = "acct:$domain@$domain";
$resourceUrlEncoded = urlencode($resource);
$this->client->request('GET', "https://$domain/.well-known/webfinger?resource=$resourceUrlEncoded");
self::assertResponseIsSuccessful();
$jsonContent = self::getJsonResponse($this->client);
self::assertResponseIsSuccessful();
self::assertArrayHasKey('subject', $jsonContent);
self::assertEquals($resource, $jsonContent['subject']);
self::assertArrayHasKey('links', $jsonContent);
self::assertNotEmpty($jsonContent['links']);
$instanceActor = $jsonContent['links'][0];
self::assertArrayKeysMatch(['rel', 'href', 'type'], $instanceActor);
$this->client->request('GET', $instanceActor['href']);
self::assertResponseIsSuccessful();
$jsonContent = self::getJsonResponse($this->client);
self::assertNotNull($jsonContent);
$keys = ['id', 'type', 'preferredUsername', 'publicKey', 'name', 'manuallyApprovesFollowers'];
foreach ($keys as $key) {
self::assertArrayHasKey($key, $jsonContent);
}
self::assertEquals($instanceActor['href'], $jsonContent['id']);
self::assertEquals('Application', $jsonContent['type']);
self::assertEquals($domain, $jsonContent['preferredUsername']);
self::assertTrue($jsonContent['manuallyApprovesFollowers']);
self::assertNotEmpty($jsonContent['publicKey']);
}
}
================================================
FILE: tests/Functional/Misc/Entry/CrosspostDetectionTest.php
================================================
getUserByUsername('JohnDoe');
$magazine1 = $this->getMagazineByName('acme1');
$entry1 = $this->createEntry('article 001', $magazine1, $user);
sleep(1);
$magazine2 = $this->getMagazineByName('acme2');
$entry2 = $this->createEntry('article 002', $magazine2, $user);
$this->entityManager->persist($entry1);
$this->entityManager->persist($entry2);
$this->entityManager->flush();
$this->client->request('GET', '/api/entries?sort=oldest');
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertSame(2, $jsonData['pagination']['count']);
self::assertNull($jsonData['items'][0]['crosspostedEntries']);
self::assertNull($jsonData['items'][1]['crosspostedEntries']);
}
public function testCrosspostsByTitle(): void
{
$user = $this->getUserByUsername('JohnDoe');
$magazine1 = $this->getMagazineByName('acme1');
$entry1 = $this->createEntry('article 001', $magazine1, $user);
sleep(1);
$magazine2 = $this->getMagazineByName('acme2');
$entry2 = $this->createEntry('article 001', $magazine2, $user);
sleep(1);
$magazine3 = $this->getMagazineByName('acme3');
$entry3 = $this->createEntry('article 001', $magazine3, $user);
sleep(1);
$magazine4 = $this->getMagazineByName('acme4');
$entry4 = $this->createEntry('article 002', $magazine4, $user);
$this->entityManager->persist($entry1);
$this->entityManager->persist($entry2);
$this->entityManager->persist($entry3);
$this->entityManager->persist($entry4);
$this->entityManager->flush();
$this->checkCrossposts([$entry1, $entry2, $entry3]);
$this->checkCrossposts([$entry4]);
}
public function testCrosspostsByUrl(): void
{
$user = $this->getUserByUsername('JohnDoe');
$magazine1 = $this->getMagazineByName('acme1');
$entry1 = $this->createEntry('article 001', $magazine1, $user, url: 'https://duckduckgo.com');
sleep(1);
$magazine2 = $this->getMagazineByName('acme2');
$entry2 = $this->createEntry('article 001', $magazine2, $user, url: 'https://duckduckgo.com');
sleep(1);
$magazine3 = $this->getMagazineByName('acme3');
$entry3 = $this->createEntry('article with url', $magazine3, $user, url: 'https://duckduckgo.com');
sleep(1);
$magazine4 = $this->getMagazineByName('acme4');
$entry4 = $this->createEntry('article 001', $magazine4, $user, url: 'https://google.com');
$this->entityManager->persist($entry1);
$this->entityManager->persist($entry2);
$this->entityManager->persist($entry3);
$this->entityManager->persist($entry4);
$this->entityManager->flush();
$this->checkCrossposts([$entry1, $entry2, $entry3]);
$this->checkCrossposts([$entry4]);
}
public function testCrosspostsByTitleWithImageFilter(): void
{
$user = $this->getUserByUsername('JohnDoe');
$img1 = $this->getKibbyImageDto();
$img2 = $this->getKibbyFlippedImageDto();
$magazine1 = $this->getMagazineByName('acme1');
$entry1 = $this->createEntry('article 001', $magazine1, $user, imageDto: $img1);
sleep(1);
$magazine2 = $this->getMagazineByName('acme2');
$entry2 = $this->createEntry('article 001', $magazine2, $user, imageDto: $img1);
sleep(1);
$magazine3 = $this->getMagazineByName('acme3');
$entry3 = $this->createEntry('article 001', $magazine3, $user, imageDto: $img2);
sleep(1);
$magazine4 = $this->getMagazineByName('acme4');
$entry4 = $this->createEntry('article 002', $magazine4, $user);
$this->entityManager->persist($entry1);
$this->entityManager->persist($entry2);
$this->entityManager->persist($entry3);
$this->entityManager->persist($entry4);
$this->entityManager->flush();
$this->checkCrossposts([$entry1, $entry2]);
$this->checkCrossposts([$entry3]);
$this->checkCrossposts([$entry4]);
}
/**
* @param Entry[] $expectedEntries
*/
private function checkCrossposts(array $expectedEntries): void
{
$this->client->request('GET', '/api/entries?sort=oldest');
self::assertResponseIsSuccessful();
foreach ($expectedEntries as $entry) {
$this->client->request('GET', '/api/entry/'.$entry->getId());
self::assertResponseIsSuccessful();
$jsonData = self::getJsonResponse($this->client);
self::assertIsArray($jsonData['crosspostedEntries']);
$crossposts = array_filter($jsonData['crosspostedEntries'], function ($actual) use ($expectedEntries, $entry) {
$match = array_filter($expectedEntries, function ($expected) use ($actual, $entry) {
return $actual['entryId'] !== $entry->getId()
&& $actual['entryId'] === $expected->getId();
});
$matchCount = \count($match);
if (0 === $matchCount) {
return false;
} elseif (1 === $matchCount) {
return true;
} else {
self::fail('crosspostedEntries contains duplicates');
}
});
self::assertCount(\count($expectedEntries) - 1, $crossposts);
}
}
}
================================================
FILE: tests/OAuth2FlowTrait.php
================================================
'code',
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scope' => $scopes,
'state' => $state,
],
encoding_type: PHP_QUERY_RFC3986
),
[
'%3A' => ':',
'%2F' => '/',
]
);
}
protected static function runAuthorizationCodeFlowToConsentPage(KernelBrowser $client, string $scopes, string $state, string $clientId = 'testclient', string $redirectToUri = 'https://localhost:3001'): void
{
$query = self::buildPrivateAuthCodeQuery($clientId, $scopes, $state, $redirectToUri);
$uri = '/authorize?'.$query;
$client->request('GET', $uri);
$redirectUri = '/consent?'.$query;
self::assertResponseRedirects($redirectUri);
$client->followRedirect();
}
protected static function runAuthorizationCodeFlowToRedirectUri(KernelBrowser $client, string $scopes, string $consent, string $state, string $clientId = 'testclient', string $redirectUri = 'https://localhost:3001'): void
{
$crawler = $client->getCrawler();
$client->submit(
$crawler->selectButton('consent')->form(
[
'consent' => $consent,
]
)
);
$query = self::buildPrivateAuthCodeQuery($clientId, $scopes, $state, $redirectUri);
$redirectUri = '/authorize?'.$query;
self::assertResponseRedirects($redirectUri);
$client->followRedirect();
self::assertResponseRedirects();
}
public static function runAuthorizationCodeFlow(KernelBrowser $client, string $consent = 'yes', string $scopes = 'read write', string $state = 'oauth2state', string $clientId = 'testclient', string $redirectUri = 'https://localhost:3001'): void
{
self::runAuthorizationCodeFlowToConsentPage($client, $scopes, $state, $clientId, $redirectUri);
self::runAuthorizationCodeFlowToRedirectUri($client, $scopes, $consent, $state, $clientId, $redirectUri);
}
public static function runAuthorizationCodeTokenFlow(KernelBrowser $client, string $clientId = 'testclient', string $clientSecret = 'testsecret', string $redirectUri = 'https://localhost:3001'): array
{
$response = $client->getResponse();
$parsedUrl = parse_url($response->headers->get('Location'));
$result = [];
parse_str($parsedUrl['query'], $result);
self::assertArrayHasKey('code', $result);
self::assertMatchesRegularExpression(self::CODE_REGEX, $result['code']);
self::assertArrayHasKey('state', $result);
self::assertEquals('oauth2state', $result['state']);
$client->request('POST', '/token', [
'grant_type' => 'authorization_code',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'code' => $result['code'],
'redirect_uri' => $redirectUri,
]);
$response = $client->getResponse();
self::assertJson($response->getContent());
return json_decode($response->getContent(), associative: true);
}
private const VERIFIER_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
protected static function getPKCECodes(): array
{
$toReturn = [];
$toReturn['verifier'] = implode(array_map(fn (string $byte) => self::VERIFIER_ALPHABET[\ord($byte) % \strlen(self::VERIFIER_ALPHABET)], str_split(random_bytes(64))));
$hash = hash('sha256', $toReturn['verifier'], binary: true);
$toReturn['challenge'] = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
return $toReturn;
}
protected static function buildPublicAuthCodeQuery(string $clientId, string $challenge, string $challengeMethod, string $scopes, string $state, string $redirectUri): string
{
return strtr(
http_build_query(
[
'response_type' => 'code',
'client_id' => $clientId,
'code_challenge' => $challenge,
'code_challenge_method' => $challengeMethod,
'redirect_uri' => $redirectUri,
'scope' => $scopes,
'state' => $state,
],
encoding_type: PHP_QUERY_RFC3986
),
[
'%3A' => ':',
'%2F' => '/',
]
);
}
protected static function runPublicAuthorizationCodeFlowToConsentPage(KernelBrowser $client, string $scopes, string $state, string $challenge, string $challengeMethod = 'S256', string $clientId = 'testpublicclient', string $redirectUri = 'https://localhost:3001'): void
{
$query = self::buildPublicAuthCodeQuery($clientId, $challenge, $challengeMethod, $scopes, $state, $redirectUri);
$uri = '/authorize?'.$query;
$client->request('GET', $uri);
$redirectUri = '/consent?'.$query;
self::assertResponseRedirects($redirectUri);
$client->followRedirect();
}
protected static function runPublicAuthorizationCodeFlowToRedirectUri(KernelBrowser $client, string $scopes, string $consent, string $state, string $challenge, string $challengeMethod = 'S256', string $clientId = 'testpublicclient', string $redirectUri = 'https://localhost:3001'): void
{
$crawler = $client->getCrawler();
$client->submit(
$crawler->selectButton('consent')->form(
[
'consent' => $consent,
]
)
);
$query = self::buildPublicAuthCodeQuery($clientId, $challenge, $challengeMethod, $scopes, $state, $redirectUri);
$redirectUri = '/authorize?'.$query;
self::assertResponseRedirects($redirectUri);
$client->followRedirect();
self::assertResponseRedirects();
}
/**
* @return array Array with PKCE challenge and verifier codes in the 'challenge' and 'verifier' keys. Verifier needs to be passed when retrieving token
*/
public static function runPublicAuthorizationCodeFlow(KernelBrowser $client, string $consent = 'yes', string $scopes = 'read write', string $state = 'oauth2state', string $clientId = 'testpublicclient', string $redirectUri = 'https://localhost:3001'): array
{
$codes = self::getPKCECodes();
self::runPublicAuthorizationCodeFlowToConsentPage($client, $scopes, $state, $codes['challenge'], clientId: $clientId, redirectUri: $redirectUri);
self::runPublicAuthorizationCodeFlowToRedirectUri($client, $scopes, $consent, $state, $codes['challenge'], clientId: $clientId, redirectUri: $redirectUri);
return $codes;
}
public static function getAuthorizationCodeTokenResponse(KernelBrowser $client, string $clientId = 'testclient', string $clientSecret = 'testsecret', string $redirectUri = 'https://localhost:3001', string $scopes = 'read write'): array
{
self::runAuthorizationCodeFlow($client, 'yes', $scopes, clientId: $clientId, redirectUri: $redirectUri);
return self::runAuthorizationCodeTokenFlow($client, $clientId, $clientSecret, $redirectUri);
}
public static function getRefreshTokenResponse(KernelBrowser $client, string $refreshToken, string $clientId = 'testclient', string $clientSecret = 'testsecret', string $redirectUri = 'https://localhost:3001', string $scopes = 'read write'): array
{
$client->request('POST', '/token', [
'grant_type' => 'refresh_token',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'refresh_token' => $refreshToken,
]);
$response = $client->getResponse();
self::assertJson($response->getContent());
return json_decode($response->getContent(), associative: true);
}
public static function runPublicAuthorizationCodeTokenFetch(KernelBrowser $client, string $verifier, string $clientId = 'testpublicclient', string $redirectUri = 'https://localhost:3001'): void
{
$response = $client->getResponse();
$parsedUrl = parse_url($response->headers->get('Location'));
$result = [];
parse_str($parsedUrl['query'], $result);
self::assertArrayHasKey('code', $result);
self::assertMatchesRegularExpression(self::CODE_REGEX, $result['code']);
self::assertArrayHasKey('state', $result);
self::assertEquals('oauth2state', $result['state']);
$client->request('POST', '/token', [
'grant_type' => 'authorization_code',
'client_id' => $clientId,
'code_verifier' => $verifier,
'code' => $result['code'],
'redirect_uri' => $redirectUri,
]);
}
public static function getPublicAuthorizationCodeTokenResponse(KernelBrowser $client, string $clientId = 'testpublicclient', string $redirectUri = 'https://localhost:3001', string $scopes = 'read write'): array
{
$pkceCodes = self::runPublicAuthorizationCodeFlow($client, 'yes', $scopes, clientId: $clientId);
self::runPublicAuthorizationCodeTokenFetch($client, $pkceCodes['verifier'], $clientId, $redirectUri);
$response = $client->getResponse();
self::assertJson($response->getContent());
return json_decode($response->getContent(), associative: true);
}
}
================================================
FILE: tests/Service/TestingApHttpClient.php
================================================
$activityObjects
*/
public array $activityObjects = [];
/**
* @phpstan-var array $collectionObjects
*/
public array $collectionObjects = [];
/**
* @phpstan-var array $webfingerObjects
*/
public array $webfingerObjects = [];
/**
* @phpstan-var array $actorObjects
*/
public array $actorObjects = [];
/**
* @var array
*/
private array $postedObjects = [];
public function getActivityObject(string $url, bool $decoded = true): array|string|null
{
if (\array_key_exists($url, $this->activityObjects)) {
return $this->activityObjects[$url];
}
return null;
}
public function getCollectionObject(string $apAddress): ?array
{
if (\array_key_exists($apAddress, $this->collectionObjects)) {
return $this->collectionObjects[$apAddress];
}
return null;
}
public function getActorObject(string $apProfileId): ?array
{
if (\array_key_exists($apProfileId, $this->actorObjects)) {
return $this->actorObjects[$apProfileId];
}
return null;
}
public function getWebfingerObject(string $url): ?array
{
if (\array_key_exists($url, $this->webfingerObjects)) {
return $this->webfingerObjects[$url];
}
return null;
}
public function fetchInstanceNodeInfoEndpoints(string $domain, bool $decoded = true): array|string|null
{
return null;
}
public function fetchInstanceNodeInfo(string $url, bool $decoded = true): array|string|null
{
return null;
}
public function post(string $url, Magazine|User $actor, ?array $body = null, bool $useOldPrivateKey = false): void
{
$this->postedObjects[] = [
'inboxUrl' => $url,
'actor' => $actor,
'payload' => $body,
];
}
/**
* @return array
*/
public function getPostedObjects(): array
{
return $this->postedObjects;
}
public function getActivityObjectCacheKey(string $url): string
{
return 'SOME_TESTING_CACHE_KEY';
}
public function getInboxUrl(string $apProfileId): string
{
$actor = $this->getActorObject($apProfileId);
if (!empty($actor)) {
return $actor['endpoints']['sharedInbox'] ?? $actor['inbox'];
} else {
throw new \LogicException("Unable to find AP actor (user or magazine) with URL: $apProfileId");
}
}
public function invalidateActorObjectCache(string $apProfileId): void
{
}
public function invalidateCollectionObjectCache(string $apAddress): void
{
}
public function getInstancePublicKey(): string
{
return 'TESTING PUBLIC KEY';
}
}
================================================
FILE: tests/Service/TestingImageManager.php
================================================
innerImageManager = new ImageManager($storageUrl, $publicUploadsFilesystem, $httpClient, $mimeTypeGuesser, $validator, $logger, $settings, $formattingExtensionRuntime, $imageCompressionQuality, $imagineCacheManager, $entityManager);
}
public function setKibbyPath(string $kibbyPath): void
{
$this->kibbyPath = $kibbyPath;
}
public function store(string $source, string $filePath): bool
{
return $this->innerImageManager->store($source, $filePath);
}
public function download(string $url): ?string
{
// always return a copy of the kibby image path
if (!file_exists(\dirname($this->kibbyPath).'/copy')) {
mkdir(\dirname($this->kibbyPath).'/copy');
}
$tmpPath = \dirname($this->kibbyPath).'/copy/'.bin2hex(random_bytes(32)).'.png';
$srcPath = \dirname($this->kibbyPath).'/'.basename($this->kibbyPath, '.png').'.png';
if (!file_exists($srcPath)) {
throw new \Exception('For some reason the kibby image got deleted');
}
copy($srcPath, $tmpPath);
return $tmpPath;
}
public function getFilePathAndName(string $file): array
{
return $this->innerImageManager->getFilePathAndName($file);
}
public function getFilePath(string $file): string
{
return $this->innerImageManager->getFilePath($file);
}
public function getFileName(string $file): string
{
return $this->innerImageManager->getFileName($file);
}
public function remove(string $path): void
{
$this->innerImageManager->remove($path);
}
public function getPath(Image $image): string
{
return $this->innerImageManager->getPath($image);
}
public function getUrl(?Image $image): ?string
{
return $this->innerImageManager->getUrl($image);
}
public function getMimetype(Image $image): string
{
return $this->innerImageManager->getMimetype($image);
}
public function deleteOrphanedFiles(ImageRepository $repository, bool $dryRun, array $ignoredPaths): iterable
{
foreach ($this->innerImageManager->deleteOrphanedFiles($repository, $dryRun, $ignoredPaths) as $deletedPath) {
yield $deletedPath;
}
}
public function compressUntilSize(string $filePath, string $extension, int $maxBytes): bool
{
return $this->innerImageManager->compressUntilSize($filePath, $extension, $maxBytes);
}
}
================================================
FILE: tests/Unit/ActivityPub/ActorHandleTest.php
================================================
assertNotNull(ActorHandle::parse($input));
}
#[DataProvider('handleProvider')]
public function testHandleIsParsedProperly(string $input, array $output): void
{
$handle = ActorHandle::parse($input);
$this->assertEquals($handle->prefix, $output['prefix']);
$this->assertEquals($handle->name, $output['name']);
$this->assertEquals($handle->host, $output['host']);
$this->assertEquals($handle->port, $output['port']);
}
public static function handleProvider(): array
{
$handleSamples = [
'user@mbin.instance' => [
'prefix' => null,
'name' => 'user',
'host' => 'mbin.instance',
'port' => null,
],
'@someone-512@mbin.instance' => [
'prefix' => '@',
'name' => 'someone-512',
'host' => 'mbin.instance',
'port' => null,
],
'!engineering@ds9.space' => [
'prefix' => '!',
'name' => 'engineering',
'host' => 'ds9.space',
'port' => null,
],
'@leon@pink.brainrot.internal:11037' => [
'prefix' => '@',
'name' => 'leon',
'host' => 'pink.brainrot.internal',
'port' => 11037,
],
];
$inputs = array_keys($handleSamples);
$outputs = array_values($handleSamples);
return array_combine(
$inputs,
array_map(fn ($input, $output) => [$input, $output], $inputs, $outputs)
);
}
}
================================================
FILE: tests/Unit/ActivityPub/CollectionExtractionTest.php
================================================
collection = [
'id' => $this->collectionUrl,
'type' => 'Collection',
'totalItems' => 3,
];
$this->testingApHttpClient->collectionObjects[$this->collectionUrl] = $this->collection;
$this->incompleteCollection = [
'id' => $this->incompleteCollectionUrl,
'type' => 'Collection',
];
$this->testingApHttpClient->collectionObjects[$this->incompleteCollectionUrl] = $this->incompleteCollection;
}
public function testCollectionId(): void
{
self::assertEquals(3, $this->activityPubManager->extractTotalAmountFromCollection($this->collection));
}
public function testCollectionArray(): void
{
self::assertEquals(3, $this->activityPubManager->extractTotalAmountFromCollection($this->collectionUrl));
}
public function testIncompleteCollectionId(): void
{
self::assertNull($this->activityPubManager->extractTotalAmountFromCollection($this->incompleteCollectionUrl));
}
public function testIncompleteCollectionArray(): void
{
self::assertNull($this->activityPubManager->extractTotalAmountFromCollection($this->incompleteCollection));
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/AddHandlerTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getAddModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testRemoveModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getRemoveModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAddPinnedPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAddPinnedPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testRemovePinnedPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getRemovePinnedPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/AnnounceTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getAnnounceAddModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceRemoveModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceRemoveModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceAddPinnedPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceAddPinnedPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceRemovePinnedPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceRemovePinnedPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreateEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreateEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreateEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreateEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreateNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreateNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreatePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreatePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreatePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreatePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreateNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreateNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceCreateMessage(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceCreateMessageActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeleteUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeleteUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeleteEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeleteEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeleteEntryByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeleteEntryByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeleteEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeleteEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeleteEntryCommentByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeleteEntryCommentByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeletePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeletePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeletePostByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeletePostByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeletePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeletePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceDeletePostCommentByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceDeletePostCommentByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostPost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostPostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUserBoostNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUserBoostNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testMagazineBoostNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getMagazineBoostNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikeEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikeEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikeEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikeEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikeNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikeNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceLikeNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceLikeNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikeEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikeEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikeEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikeEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikeNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikeNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoLikeNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoLikeNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdateUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdateUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdateMagazine(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdateMagazineActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdateEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdateEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdateEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdateEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdatePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdatePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUpdatePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUpdatePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceBlockUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceBlockUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAnnounceUndoBlockUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAnnounceUndoBlockUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/BlockTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getBlockUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/CreateTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getCreateEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreateEntryWithUrlAndImage(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreateEntryActivityWithImageAndUrl());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreateEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreateEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreateNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreateNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreatePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreatePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreatePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreatePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreateNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreateNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testCreateMessage(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getCreateMessageActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/DeleteTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getDeleteUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeleteEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeleteEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeleteEntryByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeleteEntryByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeleteEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeleteEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeleteEntryCommentByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeleteEntryCommentByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeletePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeletePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeletePostByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeletePostByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeletePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeletePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testDeletePostCommentByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getDeletePostCommentByModeratorActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/FlagTest.php
================================================
getFlagEntryActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testFlagEntryComment(): void
{
$activity = $this->getFlagEntryCommentActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testFlagNestedEntryComment(): void
{
$activity = $this->getFlagNestedEntryCommentActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testFlagPost(): void
{
$activity = $this->getFlagPostActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testFlagPostComment(): void
{
$activity = $this->getFlagPostCommentActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testFlagNestedPostComment(): void
{
$activity = $this->getFlagNestedPostCommentActivity($this->getUserByUsername('reportingUser'));
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/FollowTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getFollowUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAcceptFollowUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAcceptFollowUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testRejectFollowUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getRejectFollowUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testAcceptFollowMagazine(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getAcceptFollowMagazineActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testRejectFollowMagazine(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getRejectFollowMagazineActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AddHandlerTest__testAddModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Add",
"target": "https://kbin.test/m/test/moderators",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AddHandlerTest__testAddPinnedPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Add",
"target": "https://kbin.test/m/test/pinned",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AddHandlerTest__testRemoveModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Remove",
"target": "https://kbin.test/m/test/moderators",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AddHandlerTest__testRemovePinnedPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Remove",
"target": "https://kbin.test/m/test/pinned",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceAddModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Add",
"target": "https://kbin.test/m/test/moderators",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceAddPinnedPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Add",
"target": "https://kbin.test/m/test/pinned",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceBlockUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Block",
"actor": "https://kbin.test/u/owner",
"object": "SCRUBBED_ID",
"target": "https://kbin.test/m/test",
"summary": "some test",
"audience": "https://kbin.test/m/test",
"expires": null,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreateEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreateEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Page",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"name": "test",
"audience": "https://kbin.test/m/test",
"content": null,
"summary": "test #test",
"mediaType": "text/html",
"source": null,
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": null
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreateMessage__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/u/user2"
],
"cc": [],
"object": {
"id": "SCRUBBED_ID",
"attributedTo": "https://kbin.test/u/user",
"to": [
"https://kbin.test/u/user2"
],
"cc": [],
"type": "ChatMessage",
"published": "SCRUBBED_DATE",
"content": "some test message
\n",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown",
"content": "some test message"
}
}
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreateNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreateNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreatePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceCreatePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"stickied": false,
"content": "test
\n#test
\n",
"mediaType": "text/html",
"source": {
"content": "test\n\n #test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n#test
\n"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeleteEntryByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"summary": " "
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeleteEntryCommentByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"summary": " "
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeleteEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeleteEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeletePostByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"summary": " "
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeletePostCommentByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"summary": " "
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeletePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeletePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceDeleteUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"removeData": true
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikeEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikeEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikeNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikeNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceLikePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceRemoveModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Remove",
"target": "https://kbin.test/m/test/moderators",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceRemovePinnedPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": "SCRUBBED_ID",
"cc": [
"https://kbin.test/m/test"
],
"type": "Remove",
"target": "https://kbin.test/m/test/pinned",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoBlockUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Block",
"actor": "https://kbin.test/u/owner",
"object": "SCRUBBED_ID",
"target": "https://kbin.test/m/test",
"summary": "some test",
"audience": "https://kbin.test/m/test",
"expires": null,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikeEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikeEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikeNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikeNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUndoLikePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user2/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Page",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"name": "test",
"audience": "https://kbin.test/m/test",
"content": null,
"summary": "test #test",
"mediaType": "text/html",
"source": null,
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": null
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/owner",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"object": {
"type": "Group",
"id": "SCRUBBED_ID",
"name": "test",
"preferredUsername": "test",
"inbox": "https://kbin.test/m/test/inbox",
"outbox": "https://kbin.test/m/test/outbox",
"followers": "https://kbin.test/m/test/followers",
"featured": "https://kbin.test/m/test/pinned",
"url": "https://kbin.test/m/test",
"publicKey": "SCRUBBED_KEY",
"summary": "",
"source": {
"content": "",
"mediaType": "text/markdown"
},
"sensitive": false,
"attributedTo": "https://kbin.test/m/test/moderators",
"postingRestrictedToMods": false,
"discoverable": true,
"indexable": true,
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
},
"published": "SCRUBBED_DATE",
"updated": "SCRUBBED_DATE"
}
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdatePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdatePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"stickied": false,
"content": "test
\n#test
\n",
"mediaType": "text/html",
"source": {
"content": "test\n\n #test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n#test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testAnnounceUpdateUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Person",
"name": "user",
"preferredUsername": "user",
"inbox": "https://kbin.test/u/user/inbox",
"outbox": "https://kbin.test/u/user/outbox",
"url": "https://kbin.test/u/user",
"manuallyApprovesFollowers": false,
"discoverable": true,
"indexable": true,
"published": "SCRUBBED_DATE",
"following": "https://kbin.test/u/user/following",
"followers": "https://kbin.test/u/user/followers",
"publicKey": "SCRUBBED_KEY",
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
}
}
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers",
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testMagazineBoostPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/m/test",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"published": "SCRUBBED_DATE",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/AnnounceTest__testUserBoostPost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Announce",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"published": "SCRUBBED_DATE"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/BlockTest__testBlockUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Block",
"actor": "https://kbin.test/u/owner",
"object": "SCRUBBED_ID",
"target": "https://kbin.test/m/test",
"summary": "some test",
"audience": "https://kbin.test/m/test",
"expires": null,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntryWithUrlAndImage__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Page",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"name": "test",
"audience": "https://kbin.test/m/test",
"content": null,
"summary": "test #test",
"mediaType": "text/html",
"source": "https://joinmbin.org",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": null
},
"attachment": [
{
"href": "https://joinmbin.org",
"type": "Link"
},
{
"type": "Image",
"mediaType": "image/png",
"url": "https://kbin.test/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png",
"name": "kibby",
"blurhash": "L$Pie*?^spxt%3W.oyn*r^W-tQjG",
"focalPoint": [
0,
0
],
"width": 96,
"height": 96
}
],
"image": {
"type": "Image",
"url": "https://kbin.test/media/a8/1c/a81cc2fea35eeb232cd28fcb109b3eb5a4e52c71bce95af6650d71876c1bcbb7.png"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Page",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"name": "test",
"audience": "https://kbin.test/m/test",
"content": null,
"summary": "test #test",
"mediaType": "text/html",
"source": null,
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": null
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateMessage__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/u/user2"
],
"cc": [],
"object": {
"id": "SCRUBBED_ID",
"attributedTo": "https://kbin.test/u/user",
"to": [
"https://kbin.test/u/user2"
],
"cc": [],
"type": "ChatMessage",
"published": "SCRUBBED_DATE",
"content": "some test message
\n",
"mediaType": "text/html",
"source": {
"mediaType": "text/markdown",
"content": "some test message"
}
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreateNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreatePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/CreateTest__testCreatePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Create",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"stickied": false,
"content": "test
\n#test
\n",
"mediaType": "text/html",
"source": {
"content": "test\n\n #test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n#test
\n"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeleteEntryByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"summary": " "
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeleteEntryCommentByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"summary": " "
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeleteEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeleteEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeletePostByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"summary": " "
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeletePostCommentByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"summary": " "
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeletePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeletePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Tombstone"
},
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/DeleteTest__testDeleteUser__1.json
================================================
{
"id": "SCRUBBED_ID",
"type": "Delete",
"actor": "https://kbin.test/u/user",
"object": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"removeData": true
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagEntryComment__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagEntry__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagNestedEntryComment__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagNestedPostComment__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagPostComment__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FlagTest__testFlagPost__1.json
================================================
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "SCRUBBED_ID",
"type": "Flag",
"actor": "https://kbin.test/u/reportingUser",
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test",
"summary": null,
"content": null,
"to": [
"https://kbin.test/m/test"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testAcceptFollowMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Accept",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/m/test"
]
},
"to": [
"https://kbin.test/u/user2"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testAcceptFollowUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Accept",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/u/user"
]
},
"to": [
"https://kbin.test/u/user2"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testFollowMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testFollowUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/u/user"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testRejectFollowMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Reject",
"actor": "https://kbin.test/m/test",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/m/test"
]
},
"to": [
"https://kbin.test/u/user2"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/FollowTest__testRejectFollowUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Reject",
"actor": "https://kbin.test/u/user",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/u/user"
]
},
"to": [
"https://kbin.test/u/user2"
]
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikeEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikeEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikeNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikeNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LikeTest__testLikePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LockTest__testLockEntryByAuthor__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Lock",
"actor": "https://kbin.test/u/user",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": "SCRUBBED_ID"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LockTest__testLockEntryByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Lock",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/owner/followers"
],
"object": "SCRUBBED_ID"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LockTest__testLockPostByAuthor__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Lock",
"actor": "https://kbin.test/u/user",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": "SCRUBBED_ID"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/LockTest__testLockPostByModerator__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Lock",
"actor": "https://kbin.test/u/owner",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/owner/followers"
],
"object": "SCRUBBED_ID"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoBlockUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/owner",
"object": {
"id": "SCRUBBED_ID",
"type": "Block",
"actor": "https://kbin.test/u/owner",
"object": "SCRUBBED_ID",
"target": "https://kbin.test/m/test",
"summary": "some test",
"audience": "https://kbin.test/m/test",
"expires": null,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoFollowMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/m/test"
]
},
"to": [
"https://kbin.test/m/test"
],
"cc": []
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoFollowUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Follow",
"actor": "https://kbin.test/u/user2",
"object": "SCRUBBED_ID",
"to": [
"https://kbin.test/u/user"
]
},
"to": [
"https://kbin.test/u/user"
],
"cc": []
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikeEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikeEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikeNestedEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikeNestedPostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UndoTest__testUndoLikePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Undo",
"actor": "https://kbin.test/u/user2",
"object": {
"id": "SCRUBBED_ID",
"type": "Like",
"actor": "https://kbin.test/u/user2",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"object": "SCRUBBED_ID",
"audience": "https://kbin.test/m/test"
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user2/followers",
"https://kbin.test/m/test"
],
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateEntryComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateEntry__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Page",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"name": "test",
"audience": "https://kbin.test/m/test",
"content": null,
"summary": "test #test",
"mediaType": "text/html",
"source": null,
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": null
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateMagazine__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/owner",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/m/test/followers"
],
"object": {
"type": "Group",
"id": "SCRUBBED_ID",
"name": "test",
"preferredUsername": "test",
"inbox": "https://kbin.test/m/test/inbox",
"outbox": "https://kbin.test/m/test/outbox",
"followers": "https://kbin.test/m/test/followers",
"featured": "https://kbin.test/m/test/pinned",
"url": "https://kbin.test/m/test",
"publicKey": "SCRUBBED_KEY",
"summary": "",
"source": {
"content": "",
"mediaType": "text/markdown"
},
"sensitive": false,
"attributedTo": "https://kbin.test/m/test/moderators",
"postingRestrictedToMods": false,
"discoverable": true,
"indexable": true,
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
},
"published": "SCRUBBED_DATE",
"updated": "SCRUBBED_DATE"
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdatePostComment__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": "SCRUBBED_ID",
"to": [
"https://www.w3.org/ns/activitystreams#Public",
"https://kbin.test/u/user"
],
"cc": [
"https://kbin.test/m/test",
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"content": "test
\n",
"mediaType": "text/html",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdatePost__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Note",
"attributedTo": "https://kbin.test/u/user",
"inReplyTo": null,
"to": [
"https://kbin.test/m/test",
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"audience": "https://kbin.test/m/test",
"sensitive": false,
"stickied": false,
"content": "test
\n#test
\n",
"mediaType": "text/html",
"source": {
"content": "test\n\n #test",
"mediaType": "text/markdown"
},
"url": "SCRUBBED_ID",
"tag": [
{
"type": "Hashtag",
"href": "https://kbin.test/tag/test",
"name": "#test"
}
],
"commentsEnabled": true,
"published": "SCRUBBED_DATE",
"contentMap": {
"en": "test
\n#test
\n"
},
"object": {
"updated": "SCRUBBED_DATE"
}
},
"audience": "https://kbin.test/m/test"
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/JsonSnapshots/UpdateTest__testUpdateUser__1.json
================================================
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://kbin.test/contexts"
],
"id": "SCRUBBED_ID",
"type": "Update",
"actor": "https://kbin.test/u/user",
"published": "SCRUBBED_DATE",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://kbin.test/u/user/followers"
],
"object": {
"id": "SCRUBBED_ID",
"type": "Person",
"name": "Test User",
"preferredUsername": "user",
"inbox": "https://kbin.test/u/user/inbox",
"outbox": "https://kbin.test/u/user/outbox",
"url": "https://kbin.test/u/user",
"manuallyApprovesFollowers": false,
"discoverable": true,
"indexable": true,
"published": "SCRUBBED_DATE",
"following": "https://kbin.test/u/user/following",
"followers": "https://kbin.test/u/user/followers",
"publicKey": "SCRUBBED_KEY",
"endpoints": {
"sharedInbox": "https://kbin.test/f/inbox"
}
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/LikeTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getLikeEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLikeEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLikeEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLikeNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLikeNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLikePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLikePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLikePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLikePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLikeNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLikeNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/LockTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getLockEntryActivityByAuthor());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLockEntryByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLockEntryActivityByModerator());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLockPostByAuthor(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLockPostActivityByAuthor());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testLockPostByModerator(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getLockPostActivityByModerator());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/UndoTest.php
================================================
activityJsonBuilder->buildActivityJson($this->getUndoLikeEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoLikeEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoLikeEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoLikeNestedEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoLikeNestedEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoLikePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoLikePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoLikePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoLikePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoLikeNestedPostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoLikeNestedPostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoFollowUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoFollowUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoFollowMagazine(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoFollowMagazineActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUndoBlockUser(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUndoBlockUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/Outbox/UpdateTest.php
================================================
user->title = 'Test User';
$this->entityManager->persist($this->user);
$this->entityManager->flush();
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdateUserActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUpdateMagazine(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdateMagazineActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUpdateEntry(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdateEntryActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUpdateEntryComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdateEntryCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUpdatePost(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdatePostActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
public function testUpdatePostComment(): void
{
$json = $this->activityJsonBuilder->buildActivityJson($this->getUpdatePostCommentActivity());
$this->assertMatchesSnapshot($json, new ActivityPubJsonDriver());
}
}
================================================
FILE: tests/Unit/ActivityPub/TagMatchTest.php
================================================
settingsManager->get('KBIN_DOMAIN');
foreach ($this->domains as $domain) {
$this->settingsManager->set('KBIN_DOMAIN', $domain);
$context = $this->router->getContext();
$context->setHost($domain);
$username = 'user';
$user = $this->getUserByUsername($username);
$json = $this->personFactory->create($user);
$this->testingApHttpClient->actorObjects[$json['id']] = $json;
$userEvent = new WebfingerResponseEvent(new JsonRd(), "acct:$username@$domain", ['account' => $username]);
$this->eventDispatcher->dispatch($userEvent);
$realDomain = \sprintf(WebFingerFactory::WEBFINGER_URL, 'https', $domain, '', "$username@$domain");
$this->testingApHttpClient->webfingerObjects[$realDomain] = $userEvent->jsonRd->toArray();
$magazineName = 'mbin';
$magazine = $this->getMagazineByName($magazineName, user: $user);
$json = $this->groupFactory->create($magazine);
$this->testingApHttpClient->actorObjects[$json['id']] = $json;
$magazineEvent = new WebfingerResponseEvent(new JsonRd(), "acct:$magazineName@$domain", ['account' => $magazineName]);
$this->eventDispatcher->dispatch($magazineEvent);
$realDomain = \sprintf(WebFingerFactory::WEBFINGER_URL, 'https', $domain, '', "$magazineName@$domain");
$this->testingApHttpClient->webfingerObjects[$realDomain] = $magazineEvent->jsonRd->toArray();
$entry = $this->getEntryByTitle("test from $domain", magazine: $magazine, user: $user);
$json = $this->pageFactory->create($entry, $this->tagLinkRepository->getTagsOfContent($entry));
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$activity = $this->createWrapper->build($entry);
$create = $this->activityJsonBuilder->buildActivityJson($activity);
$this->testingApHttpClient->activityObjects[$create['id']] = $create;
$this->entityManager->remove($activity);
$this->entityManager->remove($entry);
$this->entityManager->remove($magazine);
$this->entityManager->remove($user);
$this->entityManager->flush();
$this->entityManager->clear();
$this->entries = new ArrayCollection();
$this->magazines = new ArrayCollection();
$this->users = new ArrayCollection();
}
$this->settingsManager->set('KBIN_DOMAIN', $prevDomain);
$context = $this->router->getContext();
$context->setHost($prevDomain);
$this->testingApHttpClient->actorObjects[$this->mastodonUser['id']] = $this->mastodonUser;
$this->testingApHttpClient->activityObjects[$this->mastodonPost['id']] = $this->mastodonPost;
$this->testingApHttpClient->webfingerObjects[\sprintf(WebFingerFactory::WEBFINGER_URL, 'https', 'masto.don', '', 'User@masto.don')] = $this->mastodonWebfinger;
}
public function setUp(): void
{
sort($this->domains);
parent::setUp();
$admin = $this->getUserByUsername('admin', isAdmin: true);
$this->getMagazineByName('random', user: $admin);
$this->createMockedRemoteObjects();
$user = $this->getUserByUsername('user');
$magazine = $this->getMagazineByName('matching_mbin', user: $user);
$magazine->title = 'Matching Mbin';
$magazine->tags = ['mbin'];
$this->entityManager->persist($magazine);
$this->entityManager->flush();
foreach ($this->domains as $domain) {
$this->remoteUsers[] = $this->activityPubManager->findActorOrCreate("user@$domain");
$this->remoteMagazines[] = $this->activityPubManager->findActorOrCreate("mbin@$domain");
}
foreach ($this->remoteUsers as $remoteUser) {
$this->magazineManager->subscribe($magazine, $remoteUser);
}
foreach ($this->remoteMagazines as $remoteMagazine) {
$this->magazineManager->subscribe($remoteMagazine, $user);
}
}
public function testMatching(): void
{
self::assertEquals(\sizeof($this->domains), \sizeof(array_filter($this->remoteMagazines)));
self::assertEquals(\sizeof($this->domains), \sizeof(array_filter($this->remoteUsers)));
$this->pullInRemoteEntries();
$this->pullInMastodonPost();
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedAnnounces = array_filter($postedObjects, fn ($item) => 'Announce' === $item['payload']['type']);
$targetInboxes = array_map(fn ($item) => parse_url($item['inboxUrl'], PHP_URL_HOST), $postedAnnounces);
sort($targetInboxes);
self::assertArrayIsEqualToArrayIgnoringListOfKeys($this->domains, $targetInboxes, []);
}
public function testMatchingLikeAnnouncing(): void
{
self::assertEquals(\sizeof($this->domains), \sizeof(array_filter($this->remoteMagazines)));
self::assertEquals(\sizeof($this->domains), \sizeof(array_filter($this->remoteUsers)));
$this->pullInRemoteEntries();
$this->pullInMastodonPost();
$mastodonPost = $this->postRepository->findOneBy(['apId' => $this->mastodonPost['id']]);
$user = $this->getUserByUsername('user');
$this->favouriteManager->toggle($user, $mastodonPost);
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedLikes = array_filter($postedObjects, fn ($item) => 'Like' === $item['payload']['type']);
$targetInboxes2 = array_map(fn ($item) => parse_url($item['inboxUrl'], PHP_URL_HOST), $postedLikes);
sort($targetInboxes2);
// the pure like activity is expected to be sent to the author of the post
$expectedInboxes = [...$this->domains, parse_url($mastodonPost->user->apInboxUrl, PHP_URL_HOST)];
sort($expectedInboxes);
self::assertArrayIsEqualToArrayIgnoringListOfKeys($expectedInboxes, $targetInboxes2, []);
// dispatch a remote like message, so we trigger the announcement of it
$activity = $this->likeWrapper->build($this->remoteUsers[0], $mastodonPost);
$json = $this->activityJsonBuilder->buildActivityJson($activity);
$this->testingApHttpClient->activityObjects[$json['id']] = $json;
$this->bus->dispatch(new LikeMessage($json));
$postedObjects = $this->testingApHttpClient->getPostedObjects();
$postedLikeAnnounces = array_filter($postedObjects, fn ($item) => 'Announce' === $item['payload']['type'] && 'Like' === $item['payload']['object']['type']);
$targetInboxes3 = array_map(fn ($item) => parse_url($item['inboxUrl'], PHP_URL_HOST), $postedLikeAnnounces);
sort($targetInboxes3);
// the announcement of the like is expected to be delivered only to the subscribers of the magazine,
// because we expect the pure like activity to already be sent to the author of the post by the remote server
self::assertArrayIsEqualToArrayIgnoringListOfKeys($this->domains, $targetInboxes3, []);
}
private function pullInRemoteEntries(): void
{
foreach (array_filter($this->testingApHttpClient->activityObjects, fn ($item) => 'Page' === $item['type']) as $apObject) {
$this->bus->dispatch(new CreateMessage($apObject));
$entry = $this->entryRepository->findOneBy(['apId' => $apObject['id']]);
self::assertNotNull($entry);
}
}
private function pullInMastodonPost(): void
{
$createActivity = $this->mastodonCreatePost;
$createActivity['object'] = $this->mastodonPost;
$this->bus->dispatch(new CreateMessage($this->mastodonPost, fullCreatePayload: $createActivity));
}
private array $mastodonUser = [
'id' => 'https://masto.don/users/User',
'type' => 'Person',
'following' => 'https://masto.don/users/User/following',
'followers' => 'https://masto.don/users/User/followers',
'inbox' => 'https://masto.don/users/User/inbox',
'outbox' => 'https://masto.don/users/User/outbox',
'featured' => 'https://masto.don/users/User/collections/featured',
'featuredTags' => 'https://masto.don/users/User/collections/tags',
'preferredUsername' => 'User',
'name' => 'User',
'summary' => 'Some summary
',
'url' => 'https://masto.don/@User',
'manuallyApprovesFollowers' => false,
'discoverable' => true,
'indexable' => true,
'published' => '2025-01-01T00:00:00Z',
'memorial' => false,
'publicKey' => [
'id' => 'https://masto.don/users/User#main-key',
'owner' => 'https://masto.don/users/User',
'publicKeyPem' => "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAujdiYalTtr7R1CJIVBIy\nP50V+/JX+P15o0Cz0LUOhKvJIVyeV6szQGHj6Idu74x9e3+xf9jzQRCH6eq8ASAH\nHAKwdnHfhSmKbCQaTEI5V8497/4yU9z9Zn7uJ+C1rrKVIEoGGkpt8bK8fynfR/hb\n17FctW6EnrVrvNHyW+WwbyEbyqAxwbcOYd78PhdftWEdP6D+t4+XUoF9N1XGpsGO\nrixJDzMwNqkg9Gg9l/mnCmxV367xgh8qHC0SNmwaMbWv6AV/07dHWlr0N1pXmHqo\n9YkOEy7XuH1hovBzHWEf++P1Ew4bstwdfyS/m5bcakmSe+dR3WDylW336nO88vAF\nCQIDAQAB\n-----END PUBLIC KEY-----\n",
],
'tag' => [],
'attachment' => [],
'endpoints' => [
'sharedInbox' => 'https://masto.don/inbox',
],
];
private array $mastodonCreatePost = [
'id' => 'https://masto.don/users/User/activities/create/110226274955756643',
'type' => 'Create',
'actor' => 'https://masto.don/users/User',
'to' => [
'https://www.w3.org/ns/activitystreams#Public',
],
'cc' => [
'https://masto.don/users/User/followers',
],
];
private array $mastodonPost = [
'id' => 'https://masto.don/users/User/statuses/110226274955756643',
'type' => 'Note',
'summary' => null,
'inReplyTo' => null,
'published' => '2025-01-01T15:51:18Z',
'url' => 'https://masto.don/@User/110226274955756643',
'attributedTo' => 'https://masto.don/users/User',
'to' => [
'https://www.w3.org/ns/activitystreams#Public',
],
'cc' => [
'https://masto.don/users/User/followers',
],
'sensitive' => false,
'atomUri' => 'https://masto.don/users/User/statuses/110226274955756643',
'inReplyToAtomUri' => null,
'conversation' => 'tag:masto.don,2025-01-01:objectId=399588:objectType=Conversation',
'content' => 'I am very excited about #mbin
',
'contentMap' => [
'de' => 'I am very excited about #mbin
',
],
'attachment' => [],
'tag' => [
[
'type' => 'Hashtag',
'href' => 'https://masto.don/tags/mbin',
'name' => '#mbin',
],
],
'replies' => [
'id' => 'https://masto.don/users/User/statuses/110226274955756643/replies',
'type' => 'Collection',
'first' => [
'type' => 'CollectionPage',
'next' => 'https://masto.don/users/User/statuses/110226274955756643/replies?min_id=110226283102047096&page=true',
'partOf' => 'https://masto.don/users/User/statuses/110226274955756643/replies',
'items' => [
'https://masto.don/users/User/statuses/110226283102047096',
],
],
],
'likes' => [
'id' => 'https://masto.don/users/User/statuses/110226274955756643/likes',
'type' => 'Collection',
'totalItems' => 0,
],
'shares' => [
'id' => 'https://masto.don/users/User/statuses/110226274955756643/shares',
'type' => 'Collection',
'totalItems' => 0,
],
];
private array $mastodonWebfinger = [
'subject' => 'acct:User@masto.don',
'aliases' => [
'https://masto.don/@User',
'https://masto.don/users/User',
],
'links' => [
[
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => 'https://masto.don/@User',
],
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://masto.don/users/User',
],
[
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
'template' => 'https://masto.don/authorize_interaction?uri=[uri]',
],
],
];
}
================================================
FILE: tests/Unit/ActivityPub/Traits/AddRemoveActivityGeneratorTrait.php
================================================
addRemoveFactory->buildAddModerator($this->owner, $this->user, $this->magazine);
}
public function getRemoveModeratorActivity(): Activity
{
return $this->addRemoveFactory->buildRemoveModerator($this->owner, $this->user, $this->magazine);
}
public function getAddPinnedPostActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->addRemoveFactory->buildAddPinnedPost($this->owner, $entry);
}
public function getRemovePinnedPostActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->addRemoveFactory->buildRemovePinnedPost($this->owner, $entry);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/AnnounceActivityGeneratorTrait.php
================================================
announceWrapper->build($this->magazine, $this->getAddModeratorActivity());
}
public function getAnnounceRemoveModeratorActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getRemoveModeratorActivity());
}
public function getAnnounceAddPinnedPostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getAddPinnedPostActivity());
}
public function getAnnounceRemovePinnedPostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getRemovePinnedPostActivity());
}
public function getAnnounceCreateEntryActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreateEntryActivity());
}
public function getAnnounceCreateEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreateEntryCommentActivity());
}
public function getAnnounceCreateNestedEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreateNestedEntryCommentActivity());
}
public function getAnnounceCreatePostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreatePostActivity());
}
public function getAnnounceCreatePostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreatePostCommentActivity());
}
public function getAnnounceCreateNestedPostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreateNestedPostCommentActivity());
}
public function getAnnounceCreateMessageActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getCreateMessageActivity());
}
public function getAnnounceDeleteUserActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeleteUserActivity());
}
public function getAnnounceDeleteEntryActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeleteEntryActivity());
}
public function getAnnounceDeleteEntryByModeratorActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeleteEntryByModeratorActivity());
}
public function getAnnounceDeleteEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeleteEntryCommentActivity());
}
public function getAnnounceDeleteEntryCommentByModeratorActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeleteEntryCommentByModeratorActivity());
}
public function getAnnounceDeletePostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeletePostActivity());
}
public function getAnnounceDeletePostByModeratorActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeletePostByModeratorActivity());
}
public function getAnnounceDeletePostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeletePostCommentActivity());
}
public function getAnnounceDeletePostCommentByModeratorActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getDeletePostCommentByModeratorActivity());
}
public function getUserBoostEntryActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->announceWrapper->build($this->user, $entry, true);
}
public function getMagazineBoostEntryActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->announceWrapper->build($this->magazine, $entry, true);
}
public function getUserBoostEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->announceWrapper->build($this->user, $entryComment, true);
}
public function getMagazineBoostEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->announceWrapper->build($this->magazine, $entryComment, true);
}
public function getUserBoostNestedEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$entryComment2 = $this->createEntryComment('test', entry: $entry, user: $this->user, parent: $entryComment);
return $this->announceWrapper->build($this->user, $entryComment2, true);
}
public function getMagazineBoostNestedEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$entryComment2 = $this->createEntryComment('test', entry: $entry, user: $this->user, parent: $entryComment);
return $this->announceWrapper->build($this->magazine, $entryComment2, true);
}
public function getUserBoostPostActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->announceWrapper->build($this->user, $post, true);
}
public function getMagazineBoostPostActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->announceWrapper->build($this->magazine, $post, true);
}
public function getUserBoostPostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->announceWrapper->build($this->user, $postComment, true);
}
public function getMagazineBoostPostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->announceWrapper->build($this->magazine, $postComment, true);
}
public function getUserBoostNestedPostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$postComment2 = $this->createPostComment('test', post: $post, user: $this->user, parent: $postComment);
return $this->announceWrapper->build($this->user, $postComment2, true);
}
public function getMagazineBoostNestedPostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$postComment2 = $this->createPostComment('test', post: $post, user: $this->user, parent: $postComment);
return $this->announceWrapper->build($this->magazine, $postComment2, true);
}
public function getAnnounceLikeEntryActivity(): Activity
{
$like = $this->getLikeEntryActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceLikeEntryCommentActivity(): Activity
{
$like = $this->getLikeEntryCommentActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceLikeNestedEntryCommentActivity(): Activity
{
$like = $this->getLikeNestedEntryCommentActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceLikePostActivity(): Activity
{
$like = $this->getLikePostActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceLikePostCommentActivity(): Activity
{
$like = $this->getLikePostCommentActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceLikeNestedPostCommentActivity(): Activity
{
$like = $this->getLikeNestedPostCommentActivity();
return $this->announceWrapper->build($this->magazine, $like, true);
}
public function getAnnounceUndoLikeEntryActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikeEntryActivity());
}
public function getAnnounceUndoLikeEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikeEntryCommentActivity());
}
public function getAnnounceUndoLikeNestedEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikeNestedEntryCommentActivity());
}
public function getAnnounceUndoLikePostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikePostActivity());
}
public function getAnnounceUndoLikePostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikePostCommentActivity());
}
public function getAnnounceUndoLikeNestedPostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoLikeNestedPostCommentActivity());
}
public function getAnnounceUpdateUserActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdateUserActivity());
}
public function getAnnounceUpdateMagazineActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdateMagazineActivity());
}
public function getAnnounceUpdateEntryActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdateEntryActivity());
}
public function getAnnounceUpdateEntryCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdateEntryCommentActivity());
}
public function getAnnounceUpdatePostActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdatePostActivity());
}
public function getAnnounceUpdatePostCommentActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUpdatePostCommentActivity());
}
public function getAnnounceBlockUserActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getBlockUserActivity());
}
public function getAnnounceUndoBlockUserActivity(): Activity
{
return $this->announceWrapper->build($this->magazine, $this->getUndoBlockUserActivity());
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/BlockActivityGeneratorTrait.php
================================================
magazine->addBan($this->user, $this->owner, 'some test', null);
return $this->blockFactory->createActivityFromMagazineBan($ban);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/CreateActivityGeneratorTrait.php
================================================
getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->createWrapper->build($entry);
}
public function getCreateEntryActivityWithImageAndUrl(): Activity
{
$entry = $this->getEntryByTitle('test', url: 'https://joinmbin.org', magazine: $this->magazine, user: $this->user, image: $this->getKibbyImageDto());
return $this->createWrapper->build($entry);
}
public function getCreateEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->createWrapper->build($entryComment);
}
public function getCreateNestedEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$entryComment2 = $this->createEntryComment('test', entry: $entry, user: $this->user, parent: $entryComment);
return $this->createWrapper->build($entryComment2);
}
public function getCreatePostActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->createWrapper->build($post);
}
public function getCreatePostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->createWrapper->build($postComment);
}
public function getCreateNestedPostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$postComment2 = $this->createPostComment('test', post: $post, user: $this->user, parent: $postComment);
return $this->createWrapper->build($postComment2);
}
public function getCreateMessageActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$message = $this->createMessage($user2, $this->user, 'some test message');
return $this->createWrapper->build($message);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/DeleteActivityGeneratorTrait.php
================================================
deleteWrapper->buildForUser($this->user);
}
public function getDeleteEntryActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->user, $entry);
}
public function getDeleteEntryByModeratorActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->owner, $entry);
}
public function getDeleteEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->user, $entryComment);
}
public function getDeleteEntryCommentByModeratorActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->owner, $entryComment);
}
public function getDeletePostActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->user, $post);
}
public function getDeletePostByModeratorActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->owner, $post);
}
public function getDeletePostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->user, $postComment);
}
public function getDeletePostCommentByModeratorActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->deleteWrapper->adjustDeletePayload($this->owner, $postComment);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/FlagActivityGeneratorTrait.php
================================================
getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$report = $this->reportManager->report(ReportDto::create($entry), $reportingUser);
return $this->flagFactory->build($report);
}
public function getFlagEntryCommentActivity(User $reportingUser): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$report = $this->reportManager->report(ReportDto::create($entryComment), $reportingUser);
return $this->flagFactory->build($report);
}
public function getFlagNestedEntryCommentActivity(User $reportingUser): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$entryComment2 = $this->createEntryComment('test', entry: $entry, user: $this->user, parent: $entryComment);
$report = $this->reportManager->report(ReportDto::create($entryComment2), $reportingUser);
return $this->flagFactory->build($report);
}
public function getFlagPostActivity(User $reportingUser): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$report = $this->reportManager->report(ReportDto::create($post), $reportingUser);
return $this->flagFactory->build($report);
}
public function getFlagPostCommentActivity(User $reportingUser): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$report = $this->reportManager->report(ReportDto::create($postComment), $reportingUser);
return $this->flagFactory->build($report);
}
public function getFlagNestedPostCommentActivity(User $reportingUser): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$postComment2 = $this->createPostComment('test', post: $post, user: $this->user, parent: $postComment);
$report = $this->reportManager->report(ReportDto::create($postComment2), $reportingUser);
return $this->flagFactory->build($report);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/FollowActivityGeneratorTrait.php
================================================
getUserByUsername('user2');
return $this->followWrapper->build($user2, $this->user);
}
public function getAcceptFollowUserActivity(): Activity
{
return $this->followResponseWrapper->build($this->user, $this->getFollowUserActivity());
}
public function getRejectFollowUserActivity(): Activity
{
return $this->followResponseWrapper->build($this->user, $this->getFollowUserActivity(), isReject: true);
}
public function getFollowMagazineActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
return $this->followWrapper->build($user2, $this->magazine);
}
public function getAcceptFollowMagazineActivity(): Activity
{
return $this->followResponseWrapper->build($this->magazine, $this->getFollowMagazineActivity());
}
public function getRejectFollowMagazineActivity(): Activity
{
return $this->followResponseWrapper->build($this->magazine, $this->getFollowMagazineActivity(), isReject: true);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/LikeActivityGeneratorTrait.php
================================================
getUserByUsername('user2');
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->likeWrapper->build($user2, $entry);
}
public function getLikeEntryCommentActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->likeWrapper->build($user2, $entryComment);
}
public function getLikeNestedEntryCommentActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
$entryComment2 = $this->createEntryComment('test', entry: $entry, user: $this->user, parent: $entryComment);
return $this->likeWrapper->build($user2, $entryComment2);
}
public function getLikePostActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->likeWrapper->build($user2, $post);
}
public function getLikePostCommentActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->likeWrapper->build($user2, $postComment);
}
public function getLikeNestedPostCommentActivity(): Activity
{
$user2 = $this->getUserByUsername('user2');
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
$postComment2 = $this->createPostComment('test', post: $post, user: $this->user, parent: $postComment);
return $this->likeWrapper->build($user2, $postComment2);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/LockActivityGeneratorTrait.php
================================================
getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->lockFactory->build($this->user, $entry);
}
public function getLockEntryActivityByModerator(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->lockFactory->build($this->owner, $entry);
}
public function getLockPostActivityByAuthor(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->lockFactory->build($this->user, $post);
}
public function getLockPostActivityByModerator(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->lockFactory->build($this->owner, $post);
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/UndoActivityGeneratorTrait.php
================================================
undoWrapper->build($this->getLikeEntryActivity());
}
public function getUndoLikeEntryCommentActivity(): Activity
{
return $this->undoWrapper->build($this->getLikeEntryCommentActivity());
}
public function getUndoLikeNestedEntryCommentActivity(): Activity
{
return $this->undoWrapper->build($this->getLikeNestedEntryCommentActivity());
}
public function getUndoLikePostActivity(): Activity
{
return $this->undoWrapper->build($this->getLikePostActivity());
}
public function getUndoLikePostCommentActivity(): Activity
{
return $this->undoWrapper->build($this->getLikePostCommentActivity());
}
public function getUndoLikeNestedPostCommentActivity(): Activity
{
return $this->undoWrapper->build($this->getLikeNestedPostCommentActivity());
}
public function getUndoFollowUserActivity(): Activity
{
return $this->undoWrapper->build($this->getFollowUserActivity());
}
public function getUndoFollowMagazineActivity(): Activity
{
return $this->undoWrapper->build($this->getFollowMagazineActivity());
}
public function getUndoBlockUserActivity(): Activity
{
return $this->undoWrapper->build($this->getBlockUserActivity());
}
}
================================================
FILE: tests/Unit/ActivityPub/Traits/UpdateActivityGeneratorTrait.php
================================================
updateWrapper->buildForActor($this->user);
}
public function getUpdateMagazineActivity(): Activity
{
return $this->updateWrapper->buildForActor($this->magazine, editedBy: $this->owner);
}
public function getUpdateEntryActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
return $this->updateWrapper->buildForActivity($entry);
}
public function getUpdateEntryCommentActivity(): Activity
{
$entry = $this->getEntryByTitle('test', magazine: $this->magazine, user: $this->user);
$entryComment = $this->createEntryComment('test', entry: $entry, user: $this->user);
return $this->updateWrapper->buildForActivity($entryComment);
}
public function getUpdatePostActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
return $this->updateWrapper->buildForActivity($post);
}
public function getUpdatePostCommentActivity(): Activity
{
$post = $this->createPost('test', magazine: $this->magazine, user: $this->user);
$postComment = $this->createPostComment('test', post: $post, user: $this->user);
return $this->updateWrapper->buildForActivity($postComment);
}
}
================================================
FILE: tests/Unit/CursorPaginationTest.php
================================================
simpleSetUp();
$this->cursorPagination->setCurrentPage(-1);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 0;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(3, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getNextPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 3;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(6, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getNextPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 6;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(9, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getNextPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 9;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(10, $i);
self::assertFalse($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getPreviousPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 6;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(9, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getPreviousPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 3;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(6, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$this->cursorPagination->setCurrentPage($this->cursorPagination->getPreviousPage()[0]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
$i = 0;
foreach ($currentPage as $result) {
self::assertEquals($i, $result['value']);
++$i;
}
self::assertEquals(3, $i);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
}
public function testCursorPaginationEdgeCase(): void
{
$this->confusingSetUp();
$this->cursorPagination->setCurrentPage(-1);
$currentPage = $this->cursorPagination->getCurrentPageResults();
self::assertEquals(0, $currentPage[0]['value']);
self::assertEquals(0, $currentPage[0]['value2']);
self::assertEquals(0, $currentPage[1]['value']);
self::assertEquals(1, $currentPage[1]['value2']);
self::assertEquals(0, $currentPage[2]['value']);
self::assertEquals(2, $currentPage[2]['value2']);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
$cursors = $this->cursorPagination->getNextPage();
self::assertEquals([0, 2], $cursors);
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
self::assertEquals(0, $currentPage[0]['value']);
self::assertEquals(3, $currentPage[0]['value2']);
self::assertEquals(0, $currentPage[1]['value']);
self::assertEquals(4, $currentPage[1]['value2']);
self::assertEquals(1, $currentPage[2]['value']);
self::assertEquals(5, $currentPage[2]['value2']);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$cursors = $this->cursorPagination->getNextPage();
self::assertEquals([1, 5], $cursors);
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
self::assertEquals(1, $currentPage[0]['value']);
self::assertEquals(6, $currentPage[0]['value2']);
self::assertEquals(1, $currentPage[1]['value']);
self::assertEquals(7, $currentPage[1]['value2']);
self::assertEquals(1, $currentPage[2]['value']);
self::assertEquals(8, $currentPage[2]['value2']);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$cursors = $this->cursorPagination->getPreviousPage();
self::assertEquals([0, 2], $cursors);
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
self::assertEquals(0, $currentPage[0]['value']);
self::assertEquals(3, $currentPage[0]['value2']);
self::assertEquals(0, $currentPage[1]['value']);
self::assertEquals(4, $currentPage[1]['value2']);
self::assertEquals(1, $currentPage[2]['value']);
self::assertEquals(5, $currentPage[2]['value2']);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
$cursors = $this->cursorPagination->getPreviousPage();
self::assertEquals([0, -1], $cursors);
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$currentPage = $this->cursorPagination->getCurrentPageResults();
self::assertEquals(0, $currentPage[0]['value']);
self::assertEquals(0, $currentPage[0]['value2']);
self::assertEquals(0, $currentPage[1]['value']);
self::assertEquals(1, $currentPage[1]['value2']);
self::assertEquals(0, $currentPage[2]['value']);
self::assertEquals(2, $currentPage[2]['value2']);
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
}
public function simpleSetUp(): void
{
$tempTable = 'CREATE TEMPORARY TABLE cursorTest (value INT)';
$this->entityManager->getConnection()->executeQuery($tempTable);
for ($i = 0; $i < 10; ++$i) {
$this->entityManager->getConnection()->executeQuery("INSERT INTO cursorTest(value) VALUES($i)");
}
$sql = 'SELECT * FROM cursorTest WHERE %cursor% ORDER BY %cursorSort%';
$this->cursorPagination = new CursorPagination(
new NativeQueryCursorAdapter(
$this->entityManager->getConnection(),
$sql,
'value > :cursor',
'value <= :cursor',
'value',
'value DESC',
[],
),
'value',
3
);
}
public function confusingSetUp(): void
{
$tempTable = 'CREATE TEMPORARY TABLE cursorTest (value INT, value2 INT)';
$this->entityManager->getConnection()->executeQuery($tempTable);
$this->entityManager->getConnection()->executeQuery('INSERT INTO cursorTest(value, value2) VALUES (0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9)');
$sql = 'SELECT * FROM cursorTest WHERE %cursor% OR (%cursor2%) ORDER BY %cursorSort%, %cursorSort2%';
$this->cursorPagination = new CursorPagination(
new NativeQueryCursorAdapter(
$this->entityManager->getConnection(),
$sql,
'value > :cursor',
'value < :cursor',
'value',
'value DESC',
[],
'value = :cursor AND value2 > :cursor2',
'value = :cursor AND value2 <= :cursor2',
'value2',
'value2 DESC',
),
'value',
3,
'value2'
);
}
public function realSetUp(): void
{
for ($i = 0; $i < 10; ++$i) {
$entry = $this->getEntryByTitle("Entry $i");
$ii = 10 - $i;
$entry->createdAt = new \DateTimeImmutable("now - $ii minutes");
// for debugging purposes
$this->createdEntries[$i] = "$entry->title | {$entry->createdAt->format(DATE_ATOM)}";
}
$this->entityManager->flush();
}
public function testRealPagination(): void
{
$this->realSetUp();
$criteria = new ContentPageView(1, $this->security);
$criteria->sortOption = Criteria::SORT_COMMENTED;
$criteria->perPage = 3;
$cursor = $this->contentRepository->guessInitialCursor($criteria->sortOption);
$cursor2 = $this->contentRepository->guessInitialCursor(Criteria::SORT_NEW);
$this->cursorPagination = $this->contentRepository->findByCriteriaCursored($criteria, $cursor, $cursor2);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 9', $results[0]->title);
self::assertEquals('Entry 8', $results[1]->title);
self::assertEquals('Entry 7', $results[2]->title);
$cursors = $this->cursorPagination->getNextPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 6', $results[0]->title);
self::assertEquals('Entry 5', $results[1]->title);
self::assertEquals('Entry 4', $results[2]->title);
$cursors = $this->cursorPagination->getNextPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 3', $results[0]->title);
self::assertEquals('Entry 2', $results[1]->title);
self::assertEquals('Entry 1', $results[2]->title);
$cursors = $this->cursorPagination->getNextPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertFalse($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 0', $results[0]->title);
$cursors = $this->cursorPagination->getPreviousPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 3', $results[0]->title);
self::assertEquals('Entry 2', $results[1]->title);
self::assertEquals('Entry 1', $results[2]->title);
$cursors = $this->cursorPagination->getPreviousPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertTrue($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 6', $results[0]->title);
self::assertEquals('Entry 5', $results[1]->title);
self::assertEquals('Entry 4', $results[2]->title);
$cursors = $this->cursorPagination->getPreviousPage();
$this->cursorPagination->setCurrentPage($cursors[0], $cursors[1]);
$results = $this->cursorPagination->getCurrentPageResults();
self::assertTrue($this->cursorPagination->hasNextPage());
self::assertFalse($this->cursorPagination->hasPreviousPage());
self::assertEquals('Entry 9', $results[0]->title);
self::assertEquals('Entry 8', $results[1]->title);
self::assertEquals('Entry 7', $results[2]->title);
}
}
================================================
FILE: tests/Unit/Service/ActivityPub/SignatureValidatorTest.php
================================================
2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]
);
if (false === $res) {
self::fail('Unable to generate suitable RSA key, ensure your testing environment has a correctly configured OpenSSL library');
}
$details = openssl_pkey_get_details($res);
self::$publicKeyPem = $details['key'];
openssl_pkey_export($res, $privateKey);
self::$privateKey = openssl_pkey_get_private($privateKey);
}
/**
* Sets up the test with a valid, hs2019 signed, http request body and headers.
*
* Includes the headers and signature that would be included in a request from
* a Lemmy (0.18.3) instance
*/
private function createSignedRequest(string $inbox): void
{
$this->body = [
'actor' => 'https://kbin.localhost/m/group',
'id' => 'https://kbin.localhost/f/object/1',
];
$headers = [
'(request-target)' => 'post '.$inbox,
'content-type' => 'application/activity+json',
'date' => (new \DateTimeImmutable('now'))->format('D, d M Y H:i:s \G\M\T'),
'digest' => 'SHA-256='.base64_encode(hash('sha256', json_encode($this->body), true)),
'host' => 'kbin.localhost',
];
$signingString = implode(
"\n",
array_map(function ($k, $v) {
return strtolower($k).': '.$v;
}, array_keys($headers), $headers)
);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
openssl_sign($signingString, $signature, self::$privateKey, OPENSSL_ALGO_SHA256);
$signature = base64_encode($signature);
unset($headers['(request-target)']);
$headers['signature'] = 'keyId="%s#main-key",headers="'.$signedHeaders.'",algorithm="hs2019",signature="'.$signature.'"';
array_walk($headers, function (string &$value) {
$value = [$value];
});
$this->headers = $headers;
}
#[DoesNotPerformAssertions]
public function testItValidatesACorrectlySignedRequest(): void
{
$this->createSignedRequest('/f/inbox');
$stubMagazine = $this->createStub(Magazine::class);
$stubMagazine->apProfileId = 'https://kbin.localhost/m/group';
$this->headers['signature'][0] = \sprintf($this->headers['signature'][0], 'https://kbin.localhost/m/group');
$apManager = $this->createStub(ActivityPubManager::class);
$apManager->method('findActorOrCreate')
->willReturn($stubMagazine);
$apHttpClient = $this->createStub(ApHttpClientInterface::class);
$apHttpClient->method('getActorObject')
->willReturn(
[
'publicKey' => [
'publicKeyPem' => self::$publicKeyPem,
],
],
);
$logger = $this->createStub(LoggerInterface::class);
$sut = new SignatureValidator($apManager, $apHttpClient, $logger);
$sut->validate(['uri' => '/f/inbox'], $this->headers, json_encode($this->body));
}
#[DoesNotPerformAssertions]
public function testItValidatesACorrectlySignedRequestToAPersonalInbox(): void
{
$this->createSignedRequest('/u/user/inbox');
$stubMagazine = $this->createStub(Magazine::class);
$stubMagazine->apProfileId = 'https://kbin.localhost/m/group';
$this->headers['signature'][0] = \sprintf($this->headers['signature'][0], 'https://kbin.localhost/m/group');
$apManager = $this->createStub(ActivityPubManager::class);
$apManager->method('findActorOrCreate')
->willReturn($stubMagazine);
$apHttpClient = $this->createStub(ApHttpClientInterface::class);
$apHttpClient->method('getActorObject')
->willReturn(
[
'publicKey' => [
'publicKeyPem' => self::$publicKeyPem,
],
],
);
$logger = $this->createStub(LoggerInterface::class);
$sut = new SignatureValidator($apManager, $apHttpClient, $logger);
$sut->validate(['uri' => '/u/user/inbox'], $this->headers, json_encode($this->body));
}
public function testItDoesNotValidateARequestWithDifferentBody(): void
{
$this->createSignedRequest('/f/inbox');
$stubMagazine = $this->createStub(Magazine::class);
$stubMagazine->apProfileId = 'https://kbin.localhost/m/group';
$this->headers['signature'][0] = \sprintf($this->headers['signature'][0], 'https://kbin.localhost/m/group');
$apManager = $this->createStub(ActivityPubManager::class);
$apManager->method('findActorOrCreate')
->willReturn($stubMagazine);
$apHttpClient = $this->createStub(ApHttpClientInterface::class);
$apHttpClient->method('getActorObject')
->willReturn(
[
'publicKey' => [
'publicKeyPem' => self::$publicKeyPem,
],
],
);
$logger = $this->createStub(LoggerInterface::class);
$sut = new SignatureValidator($apManager, $apHttpClient, $logger);
$badBody = [
'actor' => 'https://kbin.localhost/m/badgroup',
'id' => 'https://kbin.localhost/f/object/1',
];
$this->expectException(InvalidApSignatureException::class);
$this->expectExceptionMessage('Signature of request could not be verified.');
$sut->validate(['uri' => '/f/inbox'], $this->headers, json_encode($badBody));
}
public function testItDoesNotValidateARequestWhenDomainsDoNotMatch(): void
{
$this->createSignedRequest('/f/inbox');
$stubMagazine = $this->createStub(Magazine::class);
$stubMagazine->apProfileId = 'https://kbin.localhost/m/group';
$this->headers['signature'][0] = \sprintf($this->headers['signature'][0], 'https://kbin.localhost/m/group');
$apManager = $this->createStub(ActivityPubManager::class);
$apHttpClient = $this->createStub(ApHttpClientInterface::class);
$logger = $this->createStub(LoggerInterface::class);
$sut = new SignatureValidator($apManager, $apHttpClient, $logger);
$badBody = [
'actor' => 'https://kbin.localhost/m/group',
'id' => 'https://lemmy.localhost/activities/announce/1',
];
$this->expectException(InvalidApSignatureException::class);
$this->expectExceptionMessage('Supplied key domain does not match domain of incoming activity.');
$sut->validate(['uri' => '/f/inbox'], $this->headers, json_encode($badBody));
}
public function testItDoesNotValidateARequestWhenUrlsAreNotHTTPS(): void
{
$this->createSignedRequest('/f/inbox');
$stubMagazine = $this->createStub(Magazine::class);
$stubMagazine->apProfileId = 'http://kbin.localhost/m/group';
$this->headers['signature'][0] = \sprintf($this->headers['signature'][0], 'http://kbin.localhost/m/group');
$apManager = $this->createStub(ActivityPubManager::class);
$apHttpClient = $this->createStub(ApHttpClientInterface::class);
$logger = $this->createStub(LoggerInterface::class);
$sut = new SignatureValidator($apManager, $apHttpClient, $logger);
$badBody = [
'actor' => 'http://kbin.localhost/m/group',
'id' => 'http://kbin.localhost/f/object/1',
];
$this->expectException(InvalidApSignatureException::class);
$this->expectExceptionMessage('Necessary supplied URL does not use HTTPS.');
$sut->validate(['uri' => '/f/inbox'], $this->headers, json_encode($badBody));
}
}
================================================
FILE: tests/Unit/Service/MentionManagerTest.php
================================================
createMock(SettingsManager::class);
// Configure the stubs
$settingsManagerMock->method('get')
->with('KBIN_DOMAIN')
->willReturn('domain.tld');
$settingsManagerMock->method('getValue')
->with('KBIN_DOMAIN')
->willReturn('domain.tld');
// Replace the actual setting service with the mock in the container
$this->getContainer()->set(SettingsManager::class, $settingsManagerMock);
$manager = $this->getContainer()->get(MentionManager::class);
$this->assertEquals($output, $manager->extract($input));
}
public static function provider(): array
{
return [
['Lorem @john ipsum', ['@john']],
['@john lorem ipsum', ['@john']],
['Lorem ipsum@john', null],
['Lorem [@john](https://already.resolved.ap.url) ipsum', ['@john']],
['Lorem @john@some.instance Ipsum', ['@john@some.instance']],
['Lorem https://some.instance/@john/12345 ipsum', null], // post on another instance
['Lorem https://some.instance/@john@other.instance/12345 ipsum', null], // AP post on another instance
];
}
}
================================================
FILE: tests/Unit/Service/MonitoringParameterEncodingTest.php
================================================
prepareContextAndQuery();
$query = $prepared['query'];
$exception = null;
try {
$this->entityManager->persist($prepared['context']);
$this->entityManager->persist($query);
$this->entityManager->flush();
} catch (\Exception $e) {
// this will throw an exception because the query parameters contain invalid characters
$exception = $e;
}
self::assertNotNull($exception);
}
#[Depends('testThrowOnParameterEncoding')]
public function testNotThrowOnEscape(): void
{
$prepared = $this->prepareContextAndQuery();
$query = $prepared['query'];
$query->cleanParameterArray();
$exception = null;
try {
$this->entityManager->persist($prepared['context']);
$this->entityManager->persist($query);
$this->entityManager->flush();
} catch (\Exception $e) {
$exception = $e;
}
self::assertNull($exception);
}
private function prepareContextAndQuery(): array
{
$context = new MonitoringExecutionContext();
$context->executionType = 'test';
$context->path = 'test';
$context->handler = 'test';
$context->userType = 'anonymous';
$context->setStartedAt();
$query = new MonitoringQuery();
$query->query = 'INSERT SOME STUFF';
$query->parameters = [
// deliberately create a broken string
// see https://stackoverflow.com/questions/4663743/how-to-keep-json-encode-from-dropping-strings-with-invalid-characters
'1' => mb_convert_encoding('Düsseldorf', 'ISO-8859-1', 'UTF-8'),
];
$query->context = $context;
$query->setStartedAt();
$query->setEndedAt();
$query->setDuration();
$context->setEndedAt();
$context->setDuration();
$context->queryDurationMilliseconds = $query->getDuration();
$context->twigRenderDurationMilliseconds = 0;
$context->curlRequestDurationMilliseconds = 0;
return ['query' => $query, 'context' => $context];
}
}
================================================
FILE: tests/Unit/Service/SettingsManagerTest.php
================================================
createStub(SettingsRepository::class);
$settingsRepository->method('findAll')->willReturn([]);
$entityManager = $this->createStub(EntityManagerInterface::class);
$requestStack = $this->createStub(RequestStack::class);
$kernel = $this->createStub(KernelInterface::class);
$kernel->method('getEnvironment')->willReturn('prod');
$instanceRepository = $this->createStub(InstanceRepository::class);
$logger = $this->createStub(LoggerInterface::class);
// SUT
$manager = new SettingsManager(
entityManager: $entityManager,
repository: $settingsRepository,
requestStack: $requestStack,
kernel: $kernel,
instanceRepository: $instanceRepository,
kbinDomain: 'domain.tld',
kbinTitle: 'title',
kbinMetaTitle: 'meta title',
kbinMetaDescription: 'meta description',
kbinMetaKeywords: 'meta keywords',
kbinDefaultLang: 'en',
kbinContactEmail: 'contact@domain.tld',
kbinSenderEmail: 'sender@domain.tld',
mbinDefaultTheme: 'light',
kbinJsEnabled: true,
kbinFederationEnabled: true,
kbinRegistrationsEnabled: true,
kbinHeaderLogo: true,
kbinCaptchaEnabled: true,
kbinFederationPageEnabled: true,
kbinAdminOnlyOauthClients: true,
mbinSsoOnlyMode: false,
mbinMaxImageBytes: $setMaxImagesBytes,
mbinDownvotesMode: DownvotesMode::Enabled,
mbinNewUsersNeedApproval: false,
logger: $logger,
mbinUseFederationAllowList: false
);
// Assert
$this->assertSame('1.5 MB', $manager->getMaxImageByteString());
}
public function testGetMaxImageByteStringOverridden(): void
{
SettingsManager::resetDto();
// Set max images bytes (as if its coming from the .env)
$setMaxImagesBytes = 1572864;
$settingsRepository = $this->createStub(SettingsRepository::class);
$settingsRepository->method('findAll')->willReturn([]);
$entityManager = $this->createStub(EntityManagerInterface::class);
$requestStack = $this->createStub(RequestStack::class);
$kernel = $this->createStub(KernelInterface::class);
$kernel->method('getEnvironment')->willReturn('prod');
$instanceRepository = $this->createStub(InstanceRepository::class);
$logger = $this->createStub(LoggerInterface::class);
// SUT
$manager = new SettingsManager(
entityManager: $entityManager,
repository: $settingsRepository,
requestStack: $requestStack,
kernel: $kernel,
instanceRepository: $instanceRepository,
kbinDomain: 'domain.tld',
kbinTitle: 'title',
kbinMetaTitle: 'meta title',
kbinMetaDescription: 'meta description',
kbinMetaKeywords: 'meta keywords',
kbinDefaultLang: 'en',
kbinContactEmail: 'contact@domain.tld',
kbinSenderEmail: 'sender@domain.tld',
mbinDefaultTheme: 'light',
kbinJsEnabled: true,
kbinFederationEnabled: true,
kbinRegistrationsEnabled: true,
kbinHeaderLogo: true,
kbinCaptchaEnabled: true,
kbinFederationPageEnabled: true,
kbinAdminOnlyOauthClients: true,
mbinSsoOnlyMode: false,
mbinMaxImageBytes: $setMaxImagesBytes,
mbinDownvotesMode: DownvotesMode::Enabled,
mbinNewUsersNeedApproval: false,
logger: $logger,
mbinUseFederationAllowList: false
);
// Assert
$this->assertSame('1.57 MB', $manager->getMaxImageByteString());
}
}
================================================
FILE: tests/Unit/Service/TagExtractorTest.php
================================================
assertEquals($output, (new TagExtractor())->extract($input, 'kbin'));
}
public static function provider(): array
{
return [
['Lorem #acme ipsum', ['acme']],
['#acme lorem ipsum', ['acme']],
['Lorem #acme #kbin #acme2 ipsum', ['acme', 'acme2']],
['Lorem ipsum#example', null],
['Lorem #acme#example', ['acme']],
['Lorem #Acme #acme ipsum', ['acme']],
['Lorem ipsum', null],
['#Test1_2_3', ['test1_2_3']],
['#_123_ABC_', ['_123_abc_']],
['Teraz #zażółć #gęślą #jaźń', ['zazolc', 'gesla', 'jazn']],
['#Göbeklitepe #çarpıcı #eğlence #şarkı #ören', ['gobeklitepe', 'carpici', 'eglence', 'sarki', 'oren']],
['#Viva #España #senõr', ['viva', 'espana', 'senor']],
['#イラスト # #一次創作', ['イラスト', '一次創作']],
['#ทำตัวไม่ถูกเลยเรา', ['ทำตัวไม่ถูกเลยเรา']],
['#ไกด์ช้างม่วง #ทวิตล่ม', ['ไกด์ช้างม่วง', 'ทวิตล่ม']],
['#Synthwave', ['synthwave']],
['#シーサイドライナー', ['シーサイドライナー']],
['#ぼっち・ざ・ろっく', ['ぼっち・ざ・ろっく']],
['https://www.site.tld/somepath/#heading', null],
];
}
}
================================================
FILE: tests/Unit/TwigRuntime/FormattingExtensionRuntimeTest.php
================================================
rt = new FormattingExtensionRuntime($this->createMock(MarkdownConverter::class));
}
public function testGetShortSentenceOnlyFirstParagraph()
{
$body = trim('
This is the first paragraph which is below the limit.
And a second paragraph.
');
$actual = $this->rt->getShortSentence($body, length: 60);
$expected = 'This is the first paragraph which is below the limit. ...';
self::assertSame($expected, $actual);
}
public function testGetShortSentenceOnlyFirstParagraphLimited()
{
$body = trim('
This is the first paragraph which is over the limit.
And a second paragraph.
');
$actual = $this->rt->getShortSentence($body, length: 10);
$expected = 'This is ...';
self::assertSame($expected, $actual);
}
public function testGetShortSentenceMultipleParagraphs()
{
$body = trim('
This is the first paragraph which is below the limit.
And a second paragraph. With more than one sentence. And so on, and so on.
');
$actual = $this->rt->getShortSentence($body, length: 89, onlyFirstParagraph: false);
$expected = "This is the first paragraph which is below the limit.\n\nAnd a second paragraph. ...";
self::assertSame($expected, $actual);
}
public function testGetShortSentenceMultipleParagraphsPreLimit()
{
$body = trim('
This is the first paragraph which is below the limit.
And a second paragraph. With more than one sentence. And so on, and so on.
');
$actual = $this->rt->getShortSentence($body, length: 90, onlyFirstParagraph: false);
$expected = "This is the first paragraph which is below the limit.\n\nAnd a second paragraph. With more t...";
self::assertSame($expected, $actual);
}
#[DataProvider('provideShortenNumberData')]
public function testShortenNumber(int $number, string $expected): void
{
self::assertEquals($expected, $this->rt->abbreviateNumber($number));
}
public static function provideShortenNumberData(): array
{
return [
[
'number' => 0,
'expected' => '0',
],
[
'number' => 1234,
'expected' => '1.23K',
],
[
'number' => 123456789,
'expected' => '123.46M',
],
[
'number' => 1999,
'expected' => '2K',
],
[
'number' => 1994,
'expected' => '1.99K',
],
[
'number' => 3548,
'expected' => '3.55K',
],
[
'number' => 1234567890,
'expected' => '1.23B',
],
[
'number' => 12345678900000,
'expected' => '12345.68B',
],
];
}
}
================================================
FILE: tests/Unit/Utils/ArrayUtilTest.php
================================================
markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('search', $markdown);
self::assertStringNotContainsString('(', $markdown);
self::assertStringNotContainsString(')', $markdown);
self::assertStringNotContainsString('[', $markdown);
self::assertStringNotContainsString(']', $markdown);
self::assertStringNotContainsString('https://kbin.test2', $markdown);
}
public function testMagazineLinks2(): void
{
$text = 'Lots of activity on [!fedibridge@lemmy.dbzer0.com](https://lemmy.dbzer0.com/c/fedibridge) following Reddit paywall announcements';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('search', $markdown);
self::assertStringNotContainsString('(', $markdown);
self::assertStringNotContainsString(')', $markdown);
self::assertStringNotContainsString('[', $markdown);
self::assertStringNotContainsString(']', $markdown);
self::assertStringNotContainsString('https://lemmy.dbzer0.com', $markdown);
}
public function testLemmyMagazineLinks(): void
{
$text = 'This should belong to [!magazine@kbin.test2](https://kbin.test2/m/magazine)';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('search', $markdown);
self::assertStringNotContainsString('(', $markdown);
self::assertStringNotContainsString(')', $markdown);
self::assertStringNotContainsString('[', $markdown);
self::assertStringNotContainsString(']', $markdown);
self::assertStringNotContainsString('https://kbin.test2', $markdown);
}
public function testExternalMagazineLinks(): void
{
$text = 'This should belong to [this magazine](https://kbin.test2/m/magazine)';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
self::assertStringContainsString('https://kbin.test2', $markdown);
}
public function testMentionLink(): void
{
$text = 'Hi @admin@kbin.test2';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('search', $markdown);
self::assertStringNotContainsString('(', $markdown);
self::assertStringNotContainsString(')', $markdown);
self::assertStringNotContainsString('[', $markdown);
self::assertStringNotContainsString(']', $markdown);
self::assertStringNotContainsString('https://kbin.test2', $markdown);
}
public function testNestedMentionLink(): void
{
$text = 'Hi [@admin@kbin.test2](https://kbin.test2/u/admin)';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('search', $markdown);
self::assertStringNotContainsString('(', $markdown);
self::assertStringNotContainsString(')', $markdown);
self::assertStringNotContainsString('[', $markdown);
self::assertStringNotContainsString(']', $markdown);
self::assertStringNotContainsString('https://kbin.test2', $markdown);
}
public function testExternalMentionLink(): void
{
$text = 'You should really talk to your [instance admin](https://kbin.test2/u/admin)';
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
// assert that this community does not exist, and we get a search link for it that does not link to the external instance
self::assertStringContainsString('https://kbin.test2', $markdown);
}
public function testExternalMagazineLocalEntryLink(): void
{
$m = new Magazine('test@kbin.test2', 'test', null, null, null, false, false, null);
$m->apId = 'test@kbin.test2';
$m->apInboxUrl = 'https://kbin.test2/inbox';
$m->apPublicUrl = 'https://kbin.test2/m/test';
$m->apProfileId = 'https://kbin.test2/m/test';
$this->entityManager->persist($m);
$entry = $this->getEntryByTitle('test', magazine: $m);
$this->entityManager->flush();
$text = "Look at my post at https://kbin.test/m/test@kbin.test2/t/{$entry->getId()}/some-slug";
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
assertStringContainsString('entry-inline', $markdown);
}
public function testExternalMagazineLocalPostLink(): void
{
$m = new Magazine('test@kbin.test2', 'test', null, null, null, false, false, null);
$m->apId = 'test@kbin.test2';
$m->apInboxUrl = 'https://kbin.test2/inbox';
$m->apPublicUrl = 'https://kbin.test2/m/test';
$m->apProfileId = 'https://kbin.test2/m/test';
$this->entityManager->persist($m);
$post = $this->createPost('test', magazine: $m);
$this->entityManager->flush();
$text = "Look at my post at https://kbin.test/m/test@kbin.test2/p/{$post->getId()}/some-slug";
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
assertStringContainsString('post-inline', $markdown);
}
public function testLocalNotMatchingUrl(): void
{
$m = new Magazine('test@kbin.test2', 'test', null, null, null, false, false, null);
$m->apId = 'test@kbin.test2';
$m->apInboxUrl = 'https://kbin.test2/inbox';
$m->apPublicUrl = 'https://kbin.test2/m/test';
$m->apProfileId = 'https://kbin.test2/m/test';
$this->entityManager->persist($m);
$entry = $this->getEntryByTitle('test', magazine: $m);
$this->entityManager->flush();
$text = "Look at my post at https://kbin.test/m/test@kbin.test2/t/{$entry->getId()}/some-slug/votes";
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
assertStringContainsString("https://kbin.test/m/test@kbin.test2/t/{$entry->getId()}/some-slug/votes", $markdown);
}
public function testBracketsInLinkTitle(): void
{
$m = new Magazine('test@kbin.test2', 'test', null, null, null, false, false, null);
$m->apId = 'test@kbin.test2';
$m->apInboxUrl = 'https://kbin.test2/inbox';
$m->apPublicUrl = 'https://kbin.test2/m/test';
$m->apProfileId = 'https://kbin.test2/m/test';
$this->entityManager->persist($m);
$entry = $this->getEntryByTitle('test', magazine: $m);
$this->entityManager->flush();
$text = "[Look at my post (or not, your choice)](https://kbin.test/m/test@kbin.test2/t/{$entry->getId()}/some-slug/favourites)";
$markdown = $this->markdownConverter->convertToHtml($text, context: [MarkdownConverter::RENDER_TARGET => RenderTarget::Page]);
assertStringContainsString("https://kbin.test/m/test@kbin.test2/t/{$entry->getId()}/some-slug/favourites", $markdown);
}
}
================================================
FILE: tests/Unit/Utils/SluggerTest.php
================================================
assertEquals($output, Slugger::camelCase($input));
}
public static function provider(): array
{
return [
['Lorem ipsum', 'loremIpsum'],
['LOremIpsum', 'lOremIpsum'],
['LORemIpsum', 'lORemIpsum'],
];
}
}
================================================
FILE: tests/ValidationTrait.php
================================================
getId(), $item['magazine']['magazineId']);
self::assertArrayKeysMatch(WebTestCase::USER_SMALL_RESPONSE_KEYS, $item['moderator']);
self::assertSame($moderator->getId(), $item['moderator']['userId']);
self::assertStringMatchesFormat('%d-%d-%dT%d:%d:%d%i:00', $item['createdAt'], 'createdAt date format invalid');
self::assertContains($item['type'], MagazineLogResponseDto::LOG_TYPES, 'Log type invalid!');
switch ($item['type']) {
case 'log_entry_deleted':
case 'log_entry_restored':
self::assertArrayKeysMatch(WebTestCase::ENTRY_RESPONSE_KEYS, $item['subject']);
break;
case 'log_entry_comment_deleted':
case 'log_entry_comment_restored':
self::assertArrayKeysMatch(WebTestCase::ENTRY_COMMENT_RESPONSE_KEYS, $item['subject']);
break;
case 'log_post_deleted':
case 'log_post_restored':
self::assertArrayKeysMatch(WebTestCase::POST_RESPONSE_KEYS, $item['subject']);
break;
case 'log_post_comment_deleted':
case 'log_post_comment_restored':
self::assertArrayKeysMatch(WebTestCase::POST_COMMENT_RESPONSE_KEYS, $item['subject']);
break;
case 'log_ban':
case 'log_unban':
self::assertArrayKeysMatch(WebTestCase::BAN_RESPONSE_KEYS, $item['subject']);
break;
default:
self::assertTrue(false, 'This should not be reached');
break;
}
}
}
}
================================================
FILE: tests/WebTestCase.php
================================================
users = new ArrayCollection();
$this->magazines = new ArrayCollection();
$this->entries = new ArrayCollection();
$this->kibbyPath = \dirname(__FILE__).'/assets/kibby_emoji.png';
$this->client = static::createClient();
$this->testingApHttpClient = new TestingApHttpClient();
self::getContainer()->set(ApHttpClientInterface::class, $this->testingApHttpClient);
$this->imageManager = new TestingImageManager(
$this->getContainer()->getParameter('kbin_storage_url'),
$this->getService(Filesystem::class),
$this->getService(HttpClientInterface::class),
$this->getService(MimeTypesInterface::class),
$this->getService(ValidatorInterface::class),
$this->getService(LoggerInterface::class),
$this->getService(SettingsManager::class),
$this->getService(FormattingExtensionRuntime::class),
self::getContainer()->getParameter('mbin_image_compression_quality'),
$this->getService(CacheManager::class),
$this->getService(EntityManagerInterface::class),
);
$this->imageManager->setKibbyPath($this->kibbyPath);
self::getContainer()->set(ImageManagerInterface::class, $this->imageManager);
$this->entityManager = $this->getService(EntityManagerInterface::class);
$this->magazineManager = $this->getService(MagazineManager::class);
$this->userManager = $this->getService(UserManager::class);
$this->entryManager = $this->getService(EntryManager::class);
$this->entryCommentManager = $this->getService(EntryCommentManager::class);
$this->postManager = $this->getService(PostManager::class);
$this->postCommentManager = $this->getService(PostCommentManager::class);
$this->messageManager = $this->getService(MessageManager::class);
$this->favouriteManager = $this->getService(FavouriteManager::class);
$this->voteManager = $this->getService(VoteManager::class);
$this->settingsManager = $this->getService(SettingsManager::class);
$this->domainManager = $this->getService(DomainManager::class);
$this->reportManager = $this->getService(ReportManager::class);
$this->badgeManager = $this->getService(BadgeManager::class);
$this->notificationManager = $this->getService(NotificationManager::class);
$this->activityPubManager = $this->getService(ActivityPubManager::class);
$this->bookmarkManager = $this->getService(BookmarkManager::class);
$this->markdownConverter = $this->getService(MarkdownConverter::class);
$this->instanceManager = $this->getService(InstanceManager::class);
$this->activityJsonBuilder = $this->getService(ActivityJsonBuilder::class);
$this->mentionManager = $this->getService(MentionManager::class);
$this->security = $this->getService(Security::class);
$this->magazineRepository = $this->getService(MagazineRepository::class);
$this->entryRepository = $this->getService(EntryRepository::class);
$this->entryCommentRepository = $this->getService(EntryCommentRepository::class);
$this->postRepository = $this->getService(PostRepository::class);
$this->postCommentRepository = $this->getService(PostCommentRepository::class);
$this->imageRepository = $this->getService(ImageRepository::class);
$this->messageRepository = $this->getService(MessageRepository::class);
$this->siteRepository = $this->getService(SiteRepository::class);
$this->notificationRepository = $this->getService(NotificationRepository::class);
$this->reportRepository = $this->getService(ReportRepository::class);
$this->settingsRepository = $this->getService(SettingsRepository::class);
$this->userRepository = $this->getService(UserRepository::class);
$this->tagLinkRepository = $this->getService(TagLinkRepository::class);
$this->bookmarkRepository = $this->getService(BookmarkRepository::class);
$this->bookmarkListRepository = $this->getService(BookmarkListRepository::class);
$this->userFollowRepository = $this->getService(UserFollowRepository::class);
$this->magazineSubscriptionRepository = $this->getService(MagazineSubscriptionRepository::class);
$this->activityRepository = $this->getService(ActivityRepository::class);
$this->instanceRepository = $this->getService(InstanceRepository::class);
$this->magazineBanRepository = $this->getService(MagazineBanRepository::class);
$this->contentRepository = $this->getService(ContentRepository::class);
$this->imageFactory = $this->getService(ImageFactory::class);
$this->personFactory = $this->getService(PersonFactory::class);
$this->magazineFactory = $this->getService(MagazineFactory::class);
$this->groupFactory = $this->getService(GroupFactory::class);
$this->pageFactory = $this->getService(EntryPageFactory::class);
$this->tombstoneFactory = $this->getService(TombstoneFactory::class);
$this->createWrapper = $this->getService(CreateWrapper::class);
$this->likeWrapper = $this->getService(LikeWrapper::class);
$this->urlGenerator = $this->getService(UrlGeneratorInterface::class);
$this->translator = $this->getService(TranslatorInterface::class);
$this->eventDispatcher = $this->getService(EventDispatcherInterface::class);
$this->requestStack = $this->getService(RequestStack::class);
$this->router = $this->getService(RouterInterface::class);
$this->bus = $this->getService(MessageBusInterface::class);
$this->projectInfoService = $this->getService(ProjectInfoService::class);
$this->logger = $this->getService(LoggerInterface::class);
// clear all cache before every test
$app = new Application($this->client->getKernel());
$command = $app->get('cache:pool:clear');
$tester = new CommandTester($command);
$tester->execute(['--all' => '1']);
}
/**
* @template T
*
* @param class-string $className
*
* @return T
*/
private function getService(string $className)
{
return $this->getContainer()->get($className);
}
public static function getJsonResponse(KernelBrowser $client): array
{
$response = $client->getResponse();
self::assertJson($response->getContent());
return json_decode($response->getContent(), associative: true);
}
/**
* Checks that all values in array $keys are present as keys in array $value, and that no additional keys are included.
*/
public static function assertArrayKeysMatch(array $keys, array $value, string $message = ''): void
{
$flipped = array_flip($keys);
$difference = array_diff_key($value, $flipped);
$diffString = json_encode(array_keys($difference));
self::assertEmpty($difference, $message ? $message : "Extra keys were found in the provided array: $diffString");
$intersect = array_intersect_key($value, $flipped);
self::assertCount(\count($flipped), $intersect, $message);
}
public static function assertNotReached(string $message = 'This branch should never happen'): void
{
self::assertFalse(true, $message);
}
public static function removeTimeElements(string $content): string
{
$pattern = '/[\w \n]+<\/time>/m';
return preg_replace($pattern, '', $content);
}
protected function tearDown(): void
{
parent::tearDown();
$entityManager = $this->entityManager;
if ($entityManager->isOpen()) {
$entityManager->close();
}
}
}
================================================
FILE: tests/bootstrap.php
================================================
bootEnv(\dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG']) {
umask(0000);
}
function bootstrapDatabase(): void
{
$kernel = new Kernel('test', true);
$kernel->boot();
$application = new Application($kernel);
$application->setAutoExit(false);
$application->run(new ArrayInput([
'command' => 'cache:pool:clear',
'--all' => '1',
'--no-interaction' => true,
]));
$application->run(new ArrayInput([
'command' => 'doctrine:database:drop',
'--if-exists' => '1',
'--force' => '1',
]));
$application->run(new ArrayInput([
'command' => 'doctrine:database:create',
]));
$application->run(new ArrayInput([
'command' => 'doctrine:migrations:migrate',
'--no-interaction' => true,
]));
$application->run(new ArrayInput([
'command' => 'mbin:ap:keys:update',
'--no-interaction' => true,
]));
$application->run(new ArrayInput([
'command' => 'mbin:push:keys:update',
'--no-interaction' => true,
]));
$conn = $kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection();
if ($conn->isTransactionActive()) {
$conn->commit();
}
if ($conn->isConnected()) {
$conn->close();
}
$kernel->shutdown();
}
if (!empty($_SERVER['BOOTSTRAP_DB'])) {
bootstrapDatabase();
}
================================================
FILE: tools/composer.json
================================================
{
"require": {
"friendsofphp/php-cs-fixer": "^3.75.0"
}
}
================================================
FILE: translations/.gitignore
================================================
================================================
FILE: translations/messages.an.yaml
================================================
{}
================================================
FILE: translations/messages.ast.yaml
================================================
{}
================================================
FILE: translations/messages.bg.yaml
================================================
comment: Коментар
size: Размер
user_badge_moderator: Мод
month: Месец
reply: Отговор
save: Запазване
weeks: Седмици
subscribe: Абониране
FAQ: Често задавани въпроси
title: Заглавие
moderators: Модератори
thread: Нишка
user: Потребител
trash: Кошче
1w: 1с
default_theme: Тема по подразбиране
fediverse: Федивселена
type.link: Връзка
rss: RSS
microblog: Микроблог
user_badge_admin: Админ
no: Не
unpin: Откачване
videos: Видеа
icon: Иконка
type.photo: Снимка
online: На линия
email: Ел. поща
theme: Тема
year: Година
name: Име
yes: Да
filter.fields.names_and_descriptions: Имена и описания
username: Потребителско име
light: Светла
terms: Условия за ползване
type.article: Нишка
people: Хора
12h: 12ч
delete: Изтриване
1m: 1м
article: Нишка
url: URL адрес
faq: Често задавани въпроси
6h: 6ч
body: Тяло
user_badge_bot: Бот
threads: Нишки
about: Относно
owner: Притежател
privacy_policy: Политика за поверителност
toolbar.link: Връзка
stats: Статистика
keywords: Ключови думи
dark: Тъмна
cancel: Отказ
no_comments: Няма коментари
content: Съдържание
users: Потребители
search: Търсене
error: Грешка
logout: Излизане
replies: Отговори
photos: Снимки
1y: 1г
login: Влизане
week: Седмица
1d: 1д
settings: Настройки
add_media: Добавяне на мултимедия
add_comment: Добавяне на коментар
months: Месеца
type.video: Видео
articles: Нишки
accept: Приемане
share: Споделяне
add: Добавяне
about_instance: Относно
comments: Коментари
unsubscribe: Отписване
pin: Закачване
profile: Профил
user_badge_op: ОП
done: Готово
3h: 3ч
filter_by_type: Филтриране по тип
edit_my_profile: Редактиране на моя профил
deleted: Изтрито от автора
reputation_points: Репутационни точки
share_on_fediverse: Споделяне във Федивселената
federated_magazine_info: Тази общност е от федеративен сървър и може да е
непълна.
down_vote: Редуциране
following: Последвани
top: Топ
reports: Доклади
writing: Писане
change_view: Промяна на изгледа
created_at: Създадено
toolbar.bold: Удебеляване
federation: Федериране
toolbar.header: Заглавка
cards: Карти
comments_count: '{0}Коментара|{1}Коментар|]1,Inf[ Коментара'
you_cant_login: Забравена парола?
password: Парола
random_posts: Случайни публикации
add_new: Добавяне на нова
oldest: Най-стари
cards_view: Картиен изглед
change_password: Промяна на паролата
email_verify: Потвърди адреса на ел. поща
copy_url_to_fediverse: Копиране на оригиналния адрес
favourites: Гласове нагоре
blocked: Блокирани
delete_account: Изтрий акаунта
all: Всички
change: Промяна
new_password: Нова парола
newest: Най-нови
select_channel: Избери канал
tree_view: Дървовиден изглед
followers: Последователи
activity: Дейност
add_post: Добавяне на публикация
hot: Популярни
on: Вкл.
unfollow: Отследване
cover: Корица
subscribers: Абонирани
type.magazine: Общност
show_more: Покажи повече
down_votes: Редуцирания
login_or_email: Потребителско име или ел. поща
from_url: От url адрес
add_new_link: Добавяне на нова връзка
added: Добавено
new_password_repeat: Потвърди новата парола
related_magazines: Подобни общности
appearance: Изглед
remember_me: Запомни ме
description: Описание
current_password: Текуща парола
add_moderator: Добавяне на модератор
off: Изкл.
compact_view: Компактен изглед
block: Блокиране
table_view: Табличен изглед
help: Помощ
create_new_magazine: Създаване на нова общност
toolbar.quote: Цитат
enter_your_post: Въведи своята публикация
add_new_photo: Добавяне на нова снимка
change_my_cover: Промяна на моята корица
register: Регистриране
add_new_article: Добавяне на нова нишка
send: Изпращане
active_users: Активни хора
add_new_post: Добавяне на нова публикация
contact: За контакт
open_url_to_fediverse: Отваряне на оригиналния адрес
notifications: Известия
change_theme: Промяна на темата
instance: Инстанция
subject_reported: Съдържанието бе докладвано.
add_new_video: Добавяне на ново видео
rules: Правила
columns: Колони
alphabetically: По азбучен ред
up_votes: Подсилвания
report: Докладване
active: Активни
status: Състояние
new_email: Нов адрес на ел. поща
mod_log: Дневник на модерирането
events: Събития
more: Още
up_vote: Подсилване
related_entries: Подобни нишки
try_again: Повторен опит
resend_account_activation_email_question: Неактивен акаунт?
random_magazines: Случайни общности
links: Връзки
upload_file: Качване на файл
change_email: Промяна на адреса на ел. поща
instances: Инстанции
random_entries: Случайни нишки
markdown_howto: Как работи редакторът?
reputation: Репутация
reset_password: Нулиране на паролата
commented: Коментирани
toolbar.code: Код
subscriptions: Абонаменти
filter_by_time: Филтриране по време
edit_post: Редактиране на публикацията
joined: Присъединяване
removed: Премахнато от модератор
domain: Домейн
go_to_original_instance: Разгледай на отдалечената инстанция
change_my_avatar: Промяна на моя лик
domains: Домейни
overview: Обзор
classic_view: Класически изглед
messages: Съобщения
boost: Подсилване
empty: Празно
change_language: Промяна на езика
federated_user_info: Този профил е от федеративен сървър и може да е непълен.
pages: Страници
magazines: Общности
useful: Полезно
chat_view: Чат изглед
all_magazines: Всички общности
enter_your_comment: Въведи своя коментар
favourite: Любимо
oauth2.grant.user.notification.delete: Изчистване на твоите известия.
show_users_avatars: Показване на ликовете на потребителите
magazine: Общност
privacy: Поверителност
repeat_password: Повтори паролата
old_email: Текущ адрес на ел. поща
posts: Публикации
related_posts: Подобни публикации
boosts: Подсилвания
approve: Одобряване
toolbar.strikethrough: Зачертано
change_magazine: Промяна на общността
homepage: Начална страница
avatar: Лик
follow: Последване
toolbar.italic: Курсив
more_from_domain: Още от домейна
oauth2.grant.user.profile.edit: Редактиране на твоя профил.
approved: Одобрени
check_email: Провери своята ел. поща
already_have_account: Вече имате акаунт?
votes: Гласувания
send_message: Изпращане на лично съобщение
message: Съобщение
filter.fields.only_names: Само имена
local_and_federated: Местни и федеративни
firstname: Собствено име
tags: Етикети
reason: Причина
edit: Редактиране
oc: ОС
position_bottom: Отдолу
people_local: Тукашни
in: в
rejected: Отхвърлени
filters: Филтри
show_avatars_on_comments: Показване на ликовете в коментарите
select_magazine: Избери общност
to: до
dont_have_account: Нямате акаунт?
federated: Федеративни
moderated: Модерирани
collapse: Свиване
local: Местни
go_to_search: Към търсене
go_to_filters: Към филтрите
general: Общи
expand: Разтваряне
go_to_content: Към съдържанието
people_federated: Федеративни
pending: В очакване
reject: Отхвърляне
is_adult: 18+ / Деликатно
agree_terms: Съгласие с %terms_link_start%Общите условия%terms_link_end% и
%policy_link_start%Политиката за поверителност%policy_link_end%
note: Бележка
comment_reply_position: Позиция на отговор на коментари
badges: Значки
position_top: Отгоре
preview: Преглед
dashboard: Табло
close: Затваряне
show_thumbnails: Показване на миниатюри
flash_post_new_success: Публикацията е създадена успешно.
copy_url: Копиране на Mbin адреса
flash_comment_edit_error: Неуспешно редактиране на коментара. Нещо се обърка.
flash_comment_new_error: Неуспешно създаване на коментар. Нещо се обърка.
subscribed: Абонаменти
flash_post_new_error: Публикацията не можа да бъде създадена. Нещо се обърка.
page_width_fixed: Фикс
removed_thread_by: премахна нишка от
flash_thread_edit_success: Нишката е редактирана успешно.
restored_post_by: възстанови публикация от
flash_post_edit_error: Неуспешно редактиране на публикацията. Нещо се обърка.
page_width_auto: Авто
flash_post_edit_success: Публикацията е редактирана успешно.
meta: Мета
right: Отдясно
flash_user_edit_password_error: Неуспешна промяна на паролата.
oauth.consent.allow: Позволяване
custom_css: Персонализиран CSS
ignore_magazines_custom_css: Игнориране на персонализирания CSS на общностите
flash_thread_edit_error: Неуспешно редактиране на нишката. Нещо се обърка.
oauth.consent.deny: Отказване
flash_user_edit_email_error: Неуспешна промяна на адреса на е-поща.
page_width_max: Макс
page_width: Ширина на страницата
flash_comment_new_success: Коментарът е създаден успешно.
flash_user_settings_general_error: Неуспешно запазване на потребителските
настройки.
removed_comment_by: премахна коментар от
left: Отляво
restored_comment_by: възстанови коментар от
flash_user_edit_profile_success: Настройките на профила са запазени успешно.
subscription_sort: Подреждане
flash_user_settings_general_success: Потребителските настройки са запазени
успешно.
flash_thread_delete_success: Нишката е изтрита успешно.
restored_thread_by: възстанови нишка от
contact_email: Ел. поща за контакт
removed_post_by: премахна публикация от
show_magazines_icons: Показване на иконките на общностите
flash_user_edit_profile_error: Неуспешно запазване на настройките на профила.
kbin_bot: Mbin Агент
added_new_reply: Добави нов отговор
featured_magazines: Представени общности
mod_remove_your_thread: Модератор премахна твоя нишка
font_size: Размер на шрифта
filter.adult.hide: Скриване на деликатно съдържание
are_you_sure: Сигурен ли си?
Your account is not active: Вашият акаунт не е активен.
2fa.disable: Изключване на двуфакторното удостоверяване
added_new_comment: Добави нов коментар
toolbar.ordered_list: Подреден списък
email_confirm_content: 'Готов ли си да активираш своя Mbin акаунт? Щракни върху връзката
по-долу:'
sidebar_position: Позиция на страничната лента
image_alt: Алтернативен текст на изображението
filter.adult.show: Показване на деликатно съдържание
filter.adult.only: Само деликатно съдържание
email_confirm_header: Здравей! Потвърди своя адрес на ел. поща.
image: Изображение
sidebar: Странична лента
toolbar.unordered_list: Неподреден списък
rounded_edges: Заоблени ръбове
edited_thread: Редактира нишка
preferred_languages: Филтриране по език на нишките и публикациите
2fa.enable: Настройване на двуфакторно удостоверяване
auto_preview: Автоматичен преглед на мултимедията
wrote_message: Написа съобщение
mod_deleted_your_comment: Модератор изтри твой коментар
toolbar.mention: Споменаване
email_confirm_title: Потвърди своя адрес на ел. поща.
added_new_thread: Добави нова нишка
two_factor_backup: Резервни кодове за двуфакторно удостоверяване
edit_comment: Запазване на промените
edited_comment: Редактира коментар
mod_remove_your_post: Модератор премахна твоя публикация
password_and_2fa: Парола & 2FA
infinite_scroll: Безкрайно превъртане
email_confirm_expire: Моля, имай предвид, че връзката ще изтече след час.
2fa.backup: Твоите резервни кодове за двуфакторно удостоверяване
all_time: Цялото време
Password is invalid: Паролата е невалидна.
edited_post: Редактира публикация
added_new_post: Добави нова публикация
two_factor_authentication: Двуфакторно удостоверяване
replied_to_your_comment: Отговори на твой коментар
2fa.verify_authentication_code.label: Въведи двуфакторен код, за да потвърдиш
настройката
flash_magazine_edit_success: Общността е редактирана успешно.
toolbar.image: Изображение
new_email_repeat: Потвърди новия адрес на ел. поща
hide_adult: Скриване на деликатното съдържание
menu: Меню
dynamic_lists: Динамични списъци
report_issue: Докладване на проблем
2fa.authentication_code.label: Код за удостоверяване
2fa.code_invalid: Кодът за удостоверяване не е валиден
user_badge_global_moderator: Глобален Мод
sensitive_show: Щракни, за показване
restore: Възстановяване
sensitive_hide: Щракни, за скриване
announcement: Оповестяване
hide: Скриване
show: Показване
details: Подробности
spoiler: Спойлер
subscribers_count: '{0}Абонирани|{1}Абониран|]1,Inf[ Абонирани'
followers_count: '{0}Последователи|{1}Последовател|]1,Inf[ Последователи'
purge: Изчистване
errors.server429.title: 429 Твърде много заявки
last_active: Последно активни
update_comment: Обновяване на коментара
2fa.verify: Потвърждаване
subscription_panel_large: Голям панел
delete_magazine: Изтриване на общността
magazine_deletion: Изтриване на общност
restore_magazine: Възстановяване на общността
apply_for_moderator: Кандидатстване за модератор
sensitive_warning: Деликатно съдържание
type.smart_contract: Смарт контракт
post: Публикация
errors.server403.title: 403 Забранено
errors.server404.title: 404 Не е намерено
subscriptions_in_own_sidebar: В отделна странична лента
expires: Изтича
kbin_intro_title: Разгледай Федивселената
type_search_term: Въведи термин за търсене
deletion: Изтриване
errors.server500.title: 500 Вътрешна грешка на сървъра
show_subscriptions: Показване на абонаментите
abandoned: Изоставени
show_profile_followings: Показване на следваните потребители
notify_on_new_entry_reply: Коментари на всички нива в нишки, които съм създал
notify_on_new_post_comment_reply: Отговори на мои коментари във всички
публикации
notify_on_new_post_reply: Отговори на всички нива в публикации, които съм създал
subscription_header: Абонаменти за общности
action: Действие
related_tags: Подобни етикети
show_profile_subscriptions: Показване на абонаментите за общности
notify_on_new_posts: Нови публикации във всяка общност, за която съм абониран
add_mentions_entries: Добавяне на етикети за споменаване в нишките
add_mentions_posts: Добавяне на етикети за споменаване в публикациите
notify_on_new_entry_comment_reply: Отговори на мои коментари във всички нишки
notify_on_new_entry: Нови нишки (връзки или статии) във всяка общност, за която
съм абониран
unblock: Отблокиране
marked_for_deletion: Отбелязано за изтриване
marked_for_deletion_at: Отбелязано за изтриване на %date%
single_settings: Отделни
account_deletion_title: Изтриване на акаунта
edited: редактирано
kbin_promo_title: Създай своя собствена инстанция
kbin_promo_desc: '%link_start%Клонирай хранилището%link_end% и разработвай федивселената'
remove_user_cover: Премахване на корицата
remove_user_avatar: Премахване на лика
hidden: Скрито
edit_entry: Редактиране на нишката
add_badge: Добавяне на значка
moderation.report.approve_report_title: Одобряване на доклада
moderate: Модериране
viewing_one_signup_request: Разглеждаш само една заявка за регистрация от
%username%
federated_search_only_loggedin: Федеративното търсене е ограничено, ако не сте
влезли
federation_page_dead_description: Инстанции, до които не можахме да доставим
поне 10 дейности подред и където последната успешна доставка и получаване са
били преди повече от седмица
account_deletion_button: Изтриване на акаунта
email.delete.description: Следният потребител е поискал акаунтът му да бъде
изтрит
resend_account_activation_email: Изпращане отново на имейл за активиране на
акаунта
email_confirm_button_text: Потвърдете заявката си за промяна на паролата
email_confirm_link_help: Алтернативно, можеш да копираш и поставиш следното в
браузъра си
email.delete.title: Заявка за изтриване на потребителски акаунт
resend_account_activation_email_error: Възникна проблем при изпращането на тази
заявка. Може да няма акаунт, свързан с този имейл, или може би вече е
активиран.
show_avatars_on_comments_help: Показване/скриване на потребителски ликове при
преглед на коментари към единична нишка или публикация.
magazine_theme_appearance_background_image: Персонализирано фоново изображение,
което ще се прилага при преглед на съдържание в твоята общност.
magazine_theme_appearance_icon: Персонализирана иконка за общността.
filter_by_federation: Филтриране по статут на федерирането
flash_thread_new_success: Нишката е създадена успешно и вече е видима за другите
потребители.
mod_log_alert: ПРЕДУПРЕЖДЕНИЕ - Дневникът на модерирането може да съдържа
неприятно или стресиращо съдържание, което е било премахнато от модераторите.
Моля, бъди внимателен.
from: от
reset_check_email_desc: Ако вече съществува акаунт, свързан с твоя адрес на ел.
поща, скоро трябва да получиш писмо, съдържащо връзка, която можеш да
използваш за нулиране на паролата си. Тази връзка ще изтече след %expire%.
enabled: Включено
disabled: Изключено
oauth.consent.title: OAuth2 формуляр за съгласие
errors.server500.description: Съжаляваме, нещо се обърка от наша страна. Ако
продължаваш да виждаш тази грешка, опитай да се свържеш с притежателя на
инстанцията. Ако тази инстанция изобщо не работи, разгледай %link_start%други
Mbin инстанции%link_end% междувременно, докато проблемът бъде разрешен.
oauth.consent.app_has_permissions: вече може да извършва следните действия
comment_reply_position_help: Показване на формата за отговор на коментар в
горната или долната част на страницата. Когато е включено „безкрайно
превъртане“, позицията винаги ще се показва в горната част.
flash_post_pin_success: Публикацията е закачена успешно.
moderation.report.approve_report_confirmation: Сигурни ли сте, че искате да
одобрите този доклад?
subject_reported_exists: Това съдържание вече е докладвано.
moderation.report.reject_report_confirmation: Сигурни ли сте, че искате да
отхвърлите този доклад?
purge_content: Изчистване на съдържанието
reset_check_email_desc2: Ако не получиш писмо, провери папката си за спам.
flash_thread_unpin_success: Нишката е откачена успешно.
flash_thread_pin_success: Нишката е закачена успешно.
too_many_requests: Ограничението е превишено, моля, опитай отново по-късно.
banned: Забрани те
kbin_intro_desc: е децентрализирана платформа за агрегиране на съдържание и
микроблогинг, която работи в мрежата на Федивселената.
browsing_one_thread: Разглеждаш само една нишка в дискусията! Всички коментари
са достъпни на страницата на публикацията.
infinite_scroll_help: Автоматично зареждане на още съдържание при достигане на
дъното на страницата.
reload_to_apply: Презареди страницата, за да приложиш промените
password_confirm_header: Потвърдете заявката си за промяна на паролата.
oauth.consent.grant_permissions: Предоставяне на разрешения
magazine_theme_appearance_custom_css: Персонализиран CSS, който ще се прилага
при преглед на съдържание в твоята общност.
delete_content: Изтриване на съдържанието
remove_media: Премахване на мултимедията
account_deletion_description: Акаунтът ти ще бъде изтрит след 30 дни, освен ако
не избереш да го изтриеш незабавно. За да възстановиш акаунта си в рамките на
30 дни, влез със същите потребителски данни или се свържи с администратор.
notify_on_user_signup: Нови регистрации
solarized_dark: Слънчево тъмна
solarized_light: Слънчево светла
default_theme_auto: Светла/Тъмна (Автоматично)
solarized_auto: Слънчева (Автоматично)
show_all: Покажи всичко
flash_register_success: Добре дошъл! Твоят акаунт вече е регистриран. Още една
стъпка - провери входящата си кутия за връзка за активиране, която ще оживи
акаунта ти.
flash_magazine_new_success: Общността е създадена успешно. Вече можеш да добавяш
ново съдържание или да разгледаш административния панел на общността.
flash_mark_as_adult_success: Публикацията е отбелязана успешно като деликатна.
flash_unmark_as_adult_success: Публикацията е демаркирана успешно като
деликатна.
set_magazines_bar: Лента с общности
mentioned_you: Спомена те
filter.fields.label: Избери кои полета да се търсят
your_account_is_not_yet_approved: Твоят акаунт все още не е одобрен. Ще ти
изпратим имейл веднага щом администраторите обработят заявката ти за
регистрация.
your_account_is_not_active: Твоят акаунт не е активиран. Моля, провери имейла си
за инструкции за активиране на акаунта или поискай нов
имейл за активиране на акаунта.
toolbar.spoiler: Спойлер
federation_page_allowed_description: Известни инстанции, с които се федерираме
federation_page_disallowed_description: Инстанции, с които не се федерираме
resend_account_activation_email_success: Ако съществува акаунт, свързан с този
имейл, ще изпратим нов имейл за активиране.
resend_account_activation_email_description: Въведете адреса на ел. поща свързан
с вашия акаунт. Ще ви изпратим друг имейл за активиране.
oauth.consent.to_allow_access: За да разрешиш този достъп, щракни върху бутона
'Позволяване' по-долу
flash_post_unpin_success: Публикацията е откачена успешно.
federation_page_dead_title: Мъртви инстанции
filter.adult.label: Избери дали да се показва деликатно съдържание
account_deletion_immediate: Незабавно изтриване
read_all: Прочети всичко
oauth.consent.app_requesting_permissions: иска да извърши следните действия от
твое име
sort_by: Подреждане по
filter_by_subscription: Филтриране по абонамент
disconnected_magazine_info: Тази общност не получава обновления (последна
активност преди %days% дни).
always_disconnected_magazine_info: Тази общност не получава обновления.
subscribe_for_updates: Абонирай се, за да започнеш да получаваш обновления.
moderation.report.reject_report_title: Отхвърляне на доклада
account_settings_changed: Настройките на акаунта ти са променени успешно. Ще
трябва да влезеш отново.
show_related_magazines: Показване на случайни общности
show_related_posts: Показване на случайни публикации
admin_users_inactive: Неактивни
table_of_contents: Съдържание
search_type_all: Всичко
show_new_icons: Показване на нови иконки
flash_email_was_sent: Имейлът е изпратен успешно.
flash_email_failed_to_sent: Имейлът не можа да бъде изпратен.
magazine_is_deleted: Общността е изтрита. Можеш да я възстановиш в рамките на 30 дни.
magazine_log_mod_added: добави модератор
magazine_log_mod_removed: премахна модератор
flash_thread_tag_banned_error: Нишката не можа да бъде създадена. Съдържанието
не е позволено.
show_new_icons_help: Показване на иконка за нова общност/потребител (на 30 дни
или по-нова)
notification_title_removed_thread: Нишка беше премахната
notification_title_mention: Беше споменат
notification_title_new_thread: Нова нишка
flash_image_download_too_large_error: Изображението не можа да бъде създадено,
твърде голямо е (макс. размер %bytes%)
direct_message: Лично съобщение
notification_title_new_post: Нова публикация
2fa.setup_error: Грешка при включване на двуфакторното удостоверяване за акаунта
flash_account_settings_changed: Настройките на акаунта ти бяха променени
успешно. Ще трябва да влезеш отново.
flash_comment_edit_success: Коментарът е обновен успешно.
reported_user: Докладван потребител
cake_day: Тортен ден
notification_title_message: Ново лично съобщение
magazine_posting_restricted_to_mods_warning: Само модераторите могат да създават
нишки в тази общност
2fa.backup_codes.recommendation: Препоръчително е да запазиш копие от тях на
сигурно място.
sensitive_toggle: Превключване на видимостта на деликатно съдържание
continue_with: Продължаване със
own_content_reported_accepted: Доклад за твое съдържание беше приет.
notification_title_new_comment: Нов коментар
notification_title_removed_comment: Коментар беше премахнат
notification_title_edited_comment: Коментар беше редактиран
notification_title_edited_thread: Нишка беше редактирана
notification_title_removed_post: Публикация беше премахната
notification_title_edited_post: Публикация беше редактирана
notification_title_new_signup: Нов потребител се регистрира
notification_body_new_signup: Потребителят %u% се регистрира.
notification_body2_new_signup_approval: Трябва да одобрите заявката, преди да
могат да влязат
show_related_entries: Показване на случайни нишки
last_failed_contact: Последен неуспешен контакт
flash_posting_restricted_error: Създаването на нишки е ограничено до модератори
в тази общност, а ти не си такъв
admin_users_active: Активни
max_image_size: Максимален размер на файла
bookmark_add_to_list: Добавяне на отметка към %list%
bookmark_add_to_default_list: Добавяне на отметка към списъка по подразбиране
bookmark_remove_from_list: Премахване на отметка от %list%
bookmark_lists: Списъци с отметки
bookmark_remove_all: Премахване на всички отметки
bookmarks: Отметки
bookmarks_list: Отметки в %list%
bookmark_list_create: Създаване
bookmark_list_create_placeholder: въведи име...
bookmark_list_create_label: Име на списъка
bookmarks_list_edit: Редактиране на списъка с отметки
bookmark_list_edit: Редактиране
new_users_need_approval: Новите потребители трябва да бъдат одобрени от
администратор, преди да могат да влязат.
search_type_post: Микроблогове
search_type_entry: Нишки
select_user: Избери потребител
signup_requests: Заявки за регистрация
application_text: Обяснете защо искате да се присъедините
signup_requests_header: Заявки за регистрация
signup_requests_paragraph: Тези потребители биха искали да се присъединят към
вашия сървър. Те не могат да влязат, докато не одобрите заявката им за
регистрация.
comment_not_found: Коментарът не е намерен
notification_title_new_reply: Нов отговор
flash_thread_new_error: Нишката не можа да бъде създадена. Нещо се обърка.
deleted_by_moderator: Нишката, публикацията или коментарът е изтрит от модератор
show_active_users: Показване на активни потребители
notification_title_new_report: Създаден е нов доклад
server_software: Сървърен софтуер
last_successful_receive: Последно успешно получаване
version: Версия
last_successful_deliver: Последна успешна доставка
deleted_by_author: Нишката, публикацията или коментарът е изтрит от автора
auto: Авто.
last_updated: Последно обновено
back: Назад
reporting_user: Докладващ потребител
own_report_rejected: Твоят доклад беше отхвърлен
own_report_accepted: Твоят доклад беше приет
someone: Някой
and: и
test_push_message: Здравей, свят!
comment_default_sort: Подреждане по подразбиране на коментарите
magazine_log_entry_pinned: закачи запис
magazine_log_entry_unpinned: премахна закачен запис
compact_view_help: Компактен изглед с по-малки полета, където мултимедията е
преместена отдясно.
show_thumbnails_help: Показване на миниатюрните изображения.
show_users_avatars_help: Показване на изображението на потребителския лик.
image_lightbox_in_list_help: Когато е отметнато, щракването върху миниатюрата
показва модален прозорец с изображение. Когато не е отметнато, щракването
върху миниатюрата ще отвори нишката.
show_magazines_icons_help: Показване на иконката на общността.
front_default_sort: Подреждане по подразбиране на началната страница
2fa.backup-create.help: Можеш да създадеш нови резервни кодове за
удостоверяване; това ще направи съществуващите кодове невалидни.
image_lightbox_in_list: Миниатюрите на нишките се отварят на цял екран
2fa.backup-create.label: Създаване на нови резервни кодове за удостоверяване
test_push_notifications_button: Тестване на известията
register_push_notifications_button: Регистриране за известия
bookmark_list_make_default: Задаване по подразбиране
ownership_requests: Заявки за притежание
request_magazine_ownership: Заявете притежание на общността
unregister_push_notifications_button: Премахване на насочени известия
manually_approves_followers: Ръчно одобрява последователи
created: Създаден
mark_as_adult: Отбелязване като деликатно
unmark_as_adult: Демаркиране като деликатно
show_magazine_domains: Показване на домейните на общностите
show_user_domains: Показване на домейните на потребителите
show_top_bar: Показване на горната лента
sticky_navbar: Залепена навигационна лента
magazine_panel: Панел на общността
admin_panel: Админ панел
registrations_enabled: Регистрирането е включено
registration_disabled: Регистрирането е изключено
pinned: Закачено
federation_enabled: Федерирането е включено
flash_magazine_theme_changed_success: Изгледът на общността е обновен успешно.
flash_magazine_theme_changed_error: Неуспешно обновяване на изгледа на
общността.
new_user_description: Този потребител е нов (активен от по-малко от %days% дни)
new_magazine_description: Тази общност е нова (активна от по-малко от %days%
дни)
magazine_posting_restricted_to_mods: Ограничаване на създаването на нишки до
модератори
sidebars_same_side: Страничните ленти от една и съща страна
remove_following: Премахване на следваните
moderator_requests: Заявки за модератор
banned_instances: Забранени инстанции
filter_labels: Етикети на филтрите
purge_magazine: Изчистване на общността
remove_subscriptions: Премахване на абонаментите
purge_account: Изчистване на акаунта
cancel_request: Отказване на заявката
change_downvotes_mode: Промяна на режима на редуциране
tag: Етикет
eng: АНГЛ
oauth2.grant.moderate.magazine_admin.delete: Изтриване на някоя от притежаваните
от теб общности.
oauth2.grant.moderate.magazine_admin.moderators: Добавяне или премахване на
модератори на някоя от притежаваните от теб общности.
oauth2.grant.moderate.magazine_admin.stats: Преглед на съдържанието, гласуването
и статистиката на притежаваните от теб общности.
oauth2.grant.entry.edit: Редактиране на твоите съществуващи нишки.
oauth2.grant.entry.report: Докладване на всяка нишка.
oauth2.grant.entry_comment.create: Създаване на нови коментари в нишки.
oauth2.grant.entry_comment.vote: Гласуване нагоре, подсилване или гласуване
надолу за всеки коментар в нишка.
oauth2.grant.magazine.block: Блокиране или отблокиране на общности и преглед на
общностите, които си блокирал.
oauth2.grant.user.bookmark.add: Добавяне на отметки
oauth2.grant.user.bookmark.remove: Премахване на отметки
oauth2.grant.user.bookmark_list.read: Четене на твоите списъци с отметки
oauth2.grant.user.bookmark_list.edit: Редактиране на твоите списъци с отметки
oauth2.grant.domain.subscribe: Абониране или отписване от домейни и преглед на
домейните, за които си абониран.
oauth2.grant.moderate.entry_comment.trash: Изтриване или възстановяване на
коментари в нишки в модерираните от теб общности.
oauth2.grant.admin.entry_comment.purge: Пълно изтриване на всеки коментар в
нишка от твоята инстанция.
oauth2.grant.admin.magazine.move_entry: Преместване на нишки между общности в
твоята инстанция.
oauth2.grant.admin.user.delete: Изтриване на потребители от твоята инстанция.
oauth.client_not_granted_message_read_permission: Това приложение не е получило
разрешение да чете твоите съобщения.
auto_preview_help: Показване на прегледите на мултимедията (снимка, видео) в
по-голям размер под съдържанието.
your_account_has_been_banned: Вашият акаунт е забранен
oauth2.grant.report.general: Докладване на нишки, публикации или коментари.
oauth2.grant.post.edit: Редактиране на твоите съществуващи публикации.
schedule_delete_account: Планиране на изтриване
oauth2.grant.domain.all: Абониране за или блокиране на домейни и преглед на
домейните, за които си абониран или блокирал.
oauth2.grant.moderate.entry_comment.set_adult: Отбелязване на коментари в нишки
като деликатни в модерираните от теб общности.
oauth2.grant.moderate.post.all: Модериране на публикации в модерираните от теб
общности.
oauth2.grant.moderate.magazine.all: Управление на забрани, доклади и преглед на
изтрити елементи в модерираните от теб общности.
oauth2.grant.admin.user.all: Забраняване, потвърждаване или пълно изтриване на
потребители в твоята инстанция.
oauth2.grant.admin.magazine.all: Преместване на нишки между или пълно изтриване
на общности в твоята инстанция.
oauth2.grant.admin.magazine.purge: Пълно изтриване на общности в твоята
инстанция.
oauth.client_identifier.invalid: Невалиден OAuth клиентски идентификатор!
moderation.report.ban_user_description: Искаш ли да забраниш потребителя
(%username%), който е създал това съдържание, от тази общност?
report_subject: Предмет
bans: Забрани
oauth2.grant.moderate.magazine.reports.all: Управление на докладите в
модерираните от теб общности.
oauth2.grant.moderate.magazine.reports.read: Четене на докладите в модерираните
от теб общности.
oauth2.grant.admin.all: Извършване на всяко административно действие върху
твоята инстанция.
oauth2.grant.delete.general: Изтриване на твоите нишки, публикации или
коментари.
oauth2.grant.admin.entry.purge: Пълно изтриване на всяка нишка от твоята
инстанция.
oauth2.grant.block.general: Блокиране или отблокиране на всяка общност, домейн
или потребител и преглед на общностите, домейните и потребителите, които си
блокирал.
oauth2.grant.entry.delete: Изтриване на твоите съществуващи нишки.
oauth2.grant.entry_comment.all: Създаване, редактиране или изтриване на твоите
коментари в нишки и гласуване, подсилване или докладване на всеки коментар в
нишка.
oauth2.grant.magazine.all: Абониране за или блокиране на общности и преглед на
общностите, за които си абониран или блокирал.
oauth2.grant.post_comment.all: Създаване, редактиране или изтриване на твоите
коментари към публикации и гласуване, подсилване или докладване на всеки
коментар към публикация.
oauth2.grant.user.bookmark_list: Четене, редактиране и изтриване на твоите
списъци с отметки
magazine_panel_tags_info: Попълни само ако искаш съдържание от федивселената да
бъде включено в тази общност въз основа на етикети
return: Връщане
bot_body_content: "Добре дошъл в Mbin Агента! Този агент играе ключова роля във включването
на ActivityPub функционалността в Mbin. Той гарантира, че Mbin може да комуникира
и да се федерира с други инстанции във федивселената.\n\nActivityPub е отворен стандартен
протокол, който позволява на децентрализираните социални мрежови платформи да комуникират
и да си взаимодействат. Той позволява на потребителите на различни инстанции (сървъри)
да следват, взаимодействат и споделят съдържание във федеративната социална мрежа,
известна като федивселената. Той предоставя стандартизиран начин за потребителите
да публикуват съдържание, да следват други потребители и да участват в социални
взаимодействия като харесване, споделяне и коментиране на нишки или публикации."
suspend_account: Спиране на акаунта
oauth2.grant.user.bookmark: Добавяне и премахване на отметки
oauth2.grant.user.bookmark_list.delete: Изтриване на твоите списъци с отметки
oauth2.grant.user.profile.all: Четене и редактиране на твоя профил.
oauth2.grant.user.message.all: Четене на твоите съобщения и изпращане на
съобщения до други потребители.
oauth2.grant.admin.instance.settings.all: Преглед или обновяване на настройките
на твоята инстанция.
oauth2.grant.admin.user.purge: Пълно изтриване на потребители от твоята
инстанция.
user_suspend_desc: Спирането на акаунта ти скрива съдържанието ти в инстанцията,
но не го премахва за постоянно и можеш да го възстановиш по всяко време.
sso_registrations_enabled: SSO регистрациите са включени
restrict_magazine_creation: Ограничаване на създаването на местни общности до
администратори и глобални модератори
by: от
answered: отговорено
open_signup_request: Отваряне на заявката за регистрация
downvotes_mode: Режим на редуцирането
delete_account_desc: Изтриване на акаунта, включително отговорите на други
потребители в създадените нишки, публикации и коментари.
2fa.user_active_tfa.title: Потребителят има активно 2FA
account_suspended: Акаунтът е спрян.
2fa.add: Добавяне към моя акаунт
2fa.qr_code_link.title: Посещаването на тази връзка може да позволи на твоята
платформа да регистрира това двуфакторно удостоверяване
ban_expired: Забраната изтече
unban: Разрешаване
ban_hashtag_btn: Забраняване на хаштага
ban: Забраняване
add_ban: Добавяне на забрана
expired_at: Изтекла на
unban_account: Разрешаване на акаунта
captcha_enabled: Captcha е включено
header_logo: Лого на заглавката
mercure_enabled: Mercure е включено
oauth2.grant.moderate.magazine.ban.delete: Разрешаване на потребители в
модерираните от теб общности.
oauth2.grant.moderate.magazine.list: Четене на списъка с модерираните от теб
общности.
private_instance: Принуждаване на потребителите да влязат, преди да имат достъп
до каквото и да е съдържание
oauth2.grant.moderate.magazine.trash.read: Преглед на изтритото съдържание в
модерираните от теб общности.
oauth2.grant.moderate.magazine_admin.all: Създаване, редактиране или изтриване
на притежаваните от теб общности.
oauth2.grant.moderate.magazine.reports.action: Приемане или отхвърляне на
доклади в модерираните от теб общности.
oauth2.grant.moderate.magazine_admin.update: Редактиране на правила, описание,
състояние на деликатността или иконката на някоя от притежаваните от теб
общности.
oauth2.grant.write.general: Създаване или редактиране на твоите нишки,
публикации или коментари.
oauth2.grant.entry.create: Създаване на нови нишки.
oauth2.grant.entry.vote: Гласуване нагоре, подсилване или гласуване надолу за
всяка нишка.
oauth2.grant.post.create: Създаване на нови публикации.
oauth2.grant.user.profile.read: Четене на твоя профил.
oauth2.grant.user.all: Четене и редактиране на твоя профил, съобщения или
известия; Четене и редактиране на разрешенията, които си предоставил на други
приложения; следване или блокиране на други потребители; преглед на списъци с
потребители, които следваш или блокираш.
oauth2.grant.user.notification.all: Четене и изчистване на твоите известия.
oauth2.grant.user.notification.read: Четене на твоите известия, включително
известия за съобщения.
oauth2.grant.user.oauth_clients.edit: Редактиране на разрешенията, които си
предоставил на други OAuth2 приложения.
oauth2.grant.user.follow: Последване или отследване на потребители и четене на
списъка с потребители, които следваш.
oauth2.grant.user.block: Блокиране или отблокиране на потребители и четене на
списъка с потребители, които блокираш.
oauth2.grant.moderate.entry_comment.all: Модериране на коментари в нишки в
модерираните от теб общности.
oauth2.grant.moderate.entry.set_adult: Отбелязване на нишки като деликатни в
модерираните от теб общности.
oauth2.grant.moderate.entry.trash: Изтриване или възстановяване на нишки в
модерираните от теб общности.
oauth2.grant.admin.federation.read: Преглед на списъка с дефедерирани инстанции.
oauth2.grant.admin.federation.all: Преглед и обновяване на текущо дефедерираните
инстанции.
oauth2.grant.admin.instance.information.edit: Обновяване на страниците
„Относно“, „Често задавани въпроси“, „За контакт“, „Условия за ползване“ и
„Политика за поверителност“ на твоята инстанция.
remove_schedule_delete_account: Премахване на планираното изтриване
schedule_delete_account_desc: Планиране на изтриването на този акаунт след 30
дни. Това ще скрие потребителя и неговото съдържание, както и ще попречи на
потребителя да влезе.
remove_schedule_delete_account_desc: Премахване на планираното изтриване. Цялото
съдържание ще бъде отново достъпно и потребителят ще може да влезе.
2fa.qr_code_img.alt: QR код, който позволява настройката на двуфакторно
удостоверяване за твоя акаунт
perm: За постоянно
oauth2.grant.admin.instance.stats: Преглед на статистиката на твоята инстанция.
notification_title_ban: Получихте забрана
oauth2.grant.moderate.entry.change_language: Промяна на езика на нишките в
модерираните от теб общности.
he_banned: забрани
he_unbanned: разреши
set_magazines_bar_desc: добави имената на общностите след запетаята
set_magazines_bar_empty_desc: ако полето е празно, активните общности се
показват на лентата.
ban_hashtag_description: Забраната на хаштаг ще спре създаването на публикации с
този хаштаг, както и ще скрие съществуващите публикации с този хаштаг.
unban_hashtag_btn: Разрешаване на хаштага
unban_hashtag_description: Разрешаването на хаштаг ще позволи отново създаването
на публикации с този хаштаг. Съществуващите публикации с този хаштаг вече няма
да са скрити.
ban_account: Забраняване на акаунта
tokyo_night: Нощ в Токио
filter.origin.label: Избери произход
sticky_navbar_help: Навигационната лента ще залепне за горната част на
страницата, когато превърташ надолу.
federation_page_enabled: Страницата за федерирането е включена
oauth2.grant.moderate.magazine_admin.create: Създаване на нови общности.
restrict_oauth_clients: Ограничаване на създаването на OAuth2 клиенти до
администратори
oauth2.grant.moderate.magazine_admin.edit_theme: Редактиране на персонализирания
CSS на някоя от притежаваните от теб общности.
oauth2.grant.entry_comment.edit: Редактиране на твоите съществуващи коментари в
нишки.
oauth2.grant.entry_comment.report: Докладване на всеки коментар в нишка.
oauth2.grant.domain.block: Блокиране или отблокиране на домейни и преглед на
домейните, които си блокирал.
oauth2.grant.post.vote: Гласуване нагоре, подсилване или гласуване надолу за
всяка публикация.
oauth2.grant.post.report: Докладване на всяка публикация.
oauth2.grant.post_comment.create: Създаване на нови коментари към публикации.
oauth2.grant.post_comment.edit: Редактиране на твоите съществуващи коментари към
публикации.
oauth2.grant.post_comment.vote: Гласуване нагоре, подсилване или гласуване
надолу за всеки коментар към публикация.
oauth2.grant.post_comment.delete: Изтриване на твоите съществуващи коментари към
публикации.
oauth2.grant.user.message.read: Четене на твоите съобщения.
oauth2.grant.user.oauth_clients.read: Четене на разрешенията, които си
предоставил на други OAuth2 приложения.
oauth2.grant.moderate.all: Извършване на всяко действие по модериране, което
имаш разрешение да извършиш в модерираните от теб общности.
oauth2.grant.moderate.entry.all: Модериране на нишки в модерираните от теб
общности.
oauth2.grant.user.message.create: Изпращане на съобщения до други потребители.
oauth2.grant.moderate.entry.pin: Закачане на нишки в горната част на
модерираните от теб общности.
oauth2.grant.moderate.entry_comment.change_language: Промяна на езика на
коментарите в нишки в модерираните от теб общности.
oauth2.grant.moderate.post.change_language: Промяна на езика на публикациите в
модерираните от теб общности.
oauth2.grant.moderate.post_comment.change_language: Промяна на езика на
коментарите към публикации в модерираните от теб общности.
oauth2.grant.moderate.post_comment.trash: Изтриване или възстановяване на
коментари към публикации в модерираните от теб общности.
oauth2.grant.admin.user.ban: Забраняване или разрешаване на потребители от
твоята инстанция.
oauth2.grant.admin.user.verify: Потвърждаване на потребители в твоята инстанция.
oauth2.grant.admin.instance.all: Преглед и обновяване на настройките или
информацията за инстанцията.
oauth2.grant.admin.instance.settings.edit: Обновяване на настройките на твоята
инстанция.
oauth2.grant.admin.oauth_clients.all: Преглед или анулиране на OAuth2 клиенти,
които съществуват в твоята инстанция.
oauth2.grant.admin.federation.update: Добавяне или премахване на инстанции към
или от списъка с дефедерирани инстанции.
oauth2.grant.admin.instance.settings.read: Преглед на настройките на твоята
инстанция.
oauth2.grant.admin.oauth_clients.revoke: Анулиране на достъпа до OAuth2 клиенти
в твоята инстанция.
oauth2.grant.admin.oauth_clients.read: Преглед на OAuth2 клиентите, които
съществуват в твоята инстанция, и тяхната статистика за използване.
moderation.report.ban_user_title: Забраняване на потребителя
oauth2.grant.moderate.post.pin: Закачане на публикации в горната част на
модерираните от теб общности.
purge_content_desc: Пълно изчистване на съдържанието на потребителя, включително
изтриване на отговорите на други потребители в създадените нишки, публикации и
коментари.
delete_content_desc: Изтриване на съдържанието на потребителя, оставяйки
отговорите на други потребители в създадените нишки, публикации и коментари.
2fa.available_apps: Използвай приложение за двуфакторно удостоверяване като
%google_authenticator%, %aegis% (Android) или %raivo% (iOS), за да сканираш QR
кода.
subscription_sidebar_pop_out_right: Преместване в отделна странична лента
отдясно
sso_show_first: Показване първо на SSO на страниците за вход и регистрация
report_accepted: Доклад беше приет
open_report: Отваряне на доклада
admin_users_suspended: Спрени
admin_users_banned: Забранени
user_verify: Активиране на акаунта
count: Брой
bookmark_list_is_default: Списък по подразбиране
is_default: По подразбиране
bookmark_list_selected_list: Избран списък
email_application_rejected_body: Благодарим ти за интереса, но със съжаление те
информираме, че заявката ти за регистрация е отхвърлена.
email_verification_pending: Трябва да потвърдиш своя адрес на ел. поща, преди да
можеш да влезеш.
email_application_pending: Акаунтът ти изисква одобрение от администратор, преди
да можеш да влезеш.
email_application_approved_title: Заявката ти за регистрация е одобрена
email_application_approved_body: Твоята заявка за регистрация беше одобрена от
администратора на сървъра. Вече можеш да влезеш в сървъра на %siteName% .
email_application_rejected_title: Заявката ти за регистрация е отхвърлена
flash_application_info: Администратор трябва да одобри акаунта ти, преди да
можеш да влезеш. Ще получиш имейл, след като заявката ти за регистрация бъде
обработена.
oauth2.grant.moderate.post.set_adult: Отбелязване на публикации като деликатни в
модерираните от теб общности.
oauth2.grant.moderate.post.trash: Изтриване или възстановяване на публикации в
модерираните от теб общности.
oauth2.grant.moderate.post_comment.all: Модериране на коментари към публикации в
модерираните от теб общности.
oauth2.grant.moderate.post_comment.set_adult: Отбелязване на коментари към
публикации като деликатни в модерираните от теб общности.
account_is_suspended: Потребителският акаунт е спрян.
account_unbanned: Акаунтът е разрешен.
sso_registrations_enabled.error: Новите регистрации на акаунти с мениджъри на
самоличност на трети страни в момента са изключени.
reported: докладва
sso_only_mode: Ограничаване на влизането и регистрацията само до SSO методи
Your account has been banned: Вашият акаунт е забранен.
oauth2.grant.moderate.magazine_admin.badges: Създаване или премахване на значки
от притежаваните от теб общности.
oauth2.grant.moderate.magazine_admin.tags: Създаване или премахване на етикети
от притежаваните от теб общности.
oauth2.grant.entry.all: Създаване, редактиране или изтриване на твоите нишки и
гласуване, подсилване или докладване на всяка нишка.
oauth2.grant.magazine.subscribe: Абониране или отписване от общности и преглед
на общностите, за които си абониран.
oauth2.grant.post.all: Създаване, редактиране или изтриване на твоите
микроблогове и гласуване, подсилване или докладване на всеки микроблог.
oauth2.grant.post.delete: Изтриване на твоите съществуващи публикации.
oauth2.grant.post_comment.report: Докладване на всеки коментар към публикация.
oauth2.grant.user.oauth_clients.all: Четене и редактиране на разрешенията, които
си предоставил на други OAuth2 приложения.
subscription_sidebar_pop_out_left: Преместване в отделна странична лента отляво
unsuspend_account: Възстановяване на акаунта
account_unsuspended: Акаунтът е възстановен.
account_banned: Акаунтът е забранен.
related_entry: Подобно
2fa.remove: Премахване на 2FA
oauth2.grant.entry_comment.delete: Изтриване на твоите съществуващи коментари в
нишки.
oauth2.grant.read.general: Четене на цялото съдържание, до което имаш достъп.
oauth2.grant.vote.general: Гласуване нагоре, гласуване надолу или подсилване на
нишки, публикации или коментари.
oauth2.grant.subscribe.general: Абониране или следване на всяка общност, домейн
или потребител и преглед на общностите, домейните и потребителите, за които си
абониран.
oauth2.grant.moderate.magazine.ban.create: Забраняване на потребители в
модерираните от теб общности.
oauth2.grant.admin.post.purge: Пълно изтриване на всяка публикация от твоята
инстанция.
oauth2.grant.admin.post_comment.purge: Пълно изтриване на всеки коментар към
публикация от твоята инстанция.
oauth2.grant.moderate.magazine.ban.all: Управление на забранените потребители в
модерираните от теб общности.
oauth2.grant.moderate.magazine.ban.read: Преглед на забранените потребители в
модерираните от теб общности.
2fa.backup_codes.help: Можеш да използваш тези кодове, когато нямаш своето
устройство или приложение за двуфакторно удостоверяване. Те няма да ти
бъдат показани отново и ще можеш да използваш всеки от тях
само веднъж .
subscription_sidebar_pop_in: Преместване на абонаментите във вградения панел
2fa.manual_code_hint: Ако не можете да сканирате QR кода, въведете тайната ръчно
toolbar.emoji: Емоджи
type_search_term_url_handle: Въведи термин за търсене, уеб адрес или профил
search_type_magazine: Общности
search_type_user: Потребители
search_type_actors: Общности + Потребители
search_type_content: Нишки + Микроблогове
user_instance_defederated_info: Инстанцията на този потребител е дефедерирана.
magazine_instance_defederated_info: Инстанцията на тази общност е дефедерирана.
Следователно общността няма да получава обновления.
flash_thread_instance_banned: Инстанцията на тази общност е забранена.
show_rich_mention: Разширени споменавания
show_rich_mention_magazine: Разширени споменавания на общностите
type_search_magazine: Ограничаване на търсенето до общност...
type_search_user: Ограничаване на търсенето до автор...
btn_allow: Позволяване
allow_instance: Позволяване на инстанцията
allowed_instances: Позволени инстанции
nobody: Никой
modlog_type_entry_deleted: Нишка е изтрита
modlog_type_entry_restored: Нишка е възстановена
modlog_type_entry_comment_deleted: Нишков коментар е изтрит
modlog_type_entry_comment_restored: Нишков коментар е възстановен
modlog_type_entry_pinned: Нишка е закачена
modlog_type_entry_unpinned: Нишка е откачена
crosspost: Препубликуване
banner: Банер
magazine_theme_appearance_banner: Персонализиран банер за общността. Показва се
над всички нишки и трябва да е в широк формат (5:1 или 1500px * 300px).
flash_thread_ref_image_not_found: Изображението, посочено чрез „imageHash“, не
може да бъде намерено.
show_rich_mention_help: Изобразяване на потребителски компонент при споменаване
на потребител. Това ще включва показваното им име и профилна снимка.
show_rich_mention_magazine_help: Изобразяване на компонент на общността при
споменаване. Това ще включва показваното име и иконка.
show_rich_ap_link: Богати AP връзки
attitude: Отношение
modlog_type_post_deleted: Микроблог е изтрит
modlog_type_post_restored: Микроблог е възстановен
modlog_type_post_comment_deleted: Микроблогов отговор е изтрит
modlog_type_post_comment_restored: Микроблогов отговор е възстановен
modlog_type_ban: Потребител получи забрана от общност
modlog_type_moderator_add: Добавен е модератор на общност
modlog_type_moderator_remove: Премахнат е модератор на общност
show_rich_ap_link_help: Изобразяване на вграден компонент, когато е свързано
друго съдържание на ActivityPub.
everyone: Всеки
followers_only: Само последователи
delete_magazine_icon: Изтриване на иконката на общността
flash_magazine_theme_icon_detached_success: Иконката на общността е изтрита
успешно
delete_magazine_banner: Изтриване на банера на общността
flash_magazine_theme_banner_detached_success: Банерът на общността е изтрит
успешно
their_user_follows: Брой потребители от тяхната инстанция, следващи потребители
от нашата инстанция
our_user_follows: Брой потребители от нашата инстанция, следващи потребители от
тяхната инстанция
their_magazine_subscriptions: Брой потребители от тяхната инстанция, абонирани
за общности от нашата инстанция
our_magazine_subscriptions: Брой потребители от нашата инстанция, абонирани за
общности от тяхната инстанция
btn_deny: Отказване
ban_instance: Забраняване на инстанцията
default_content_threads: Нишки
default_content_microblog: Микроблог
combined: Обединено
direct_message_setting_label: Кой може да ви изпраща лично съобщение
default_content_default: По подразбиране (Нишки)
front_default_content: Изглед по подразбиране на началната страница
federation_uses_allowlist: Използване на списък с разрешени инстанции за
федериране
defederating_instance: Дефедериране от инстанция %i
confirm_defederation: Потвърдете дефедериране
flash_error_defederation_must_confirm: Трябва да потвърдите дефедерирането
federation_page_use_allowlist_help: Ако се използва списък с разрешени
инстанции, тази инстанция ще се федерира само с изрично разрешените инстанции.
В противен случай тази инстанция ще се федерира с всички инстанции, с
изключение на тези, които са забранени.
default_content_combined: Нишки + Микроблог
sidebar_sections_random_local_only: Ограничаване на секциите в страничната лента
„Случайни нишки/публикации“ само до местни
sidebar_sections_users_local_only: Ограничаване на секцията в страничната лента
„Активни хора“ само до местни
random_local_only_performance_warning: Включването на „Случайни (само местни)“
може да повлияе на производителността на SQL.
================================================
FILE: translations/messages.ca.yaml
================================================
type.link: Enllaç
type.article: Fil
type.photo: Foto
type.video: Vídeo
type.smart_contract: Contracte intel·ligent
type.magazine: Revista
thread: Fil
threads: Fils
microblog: Microblog
people: Gent
events: Esdeveniments
magazine: Revista
magazines: Revistes
search: Cercar
add: Afegir
select_channel: Trieu un canal
sort_by: Ordenar per
hot: Popular
newest: Més nou
oldest: Més vell
commented: Comentat
change_view: Canviar vista
filter_by_time: Filtrar per temps
filter_by_type: Filtrar per tipus
filter_by_subscription: Filtrar per subscripció
filter_by_federation: Filtrar per estat de federació
comments_count: '{0}Comentaris|{1}Comentari|]1,Inf[ Comentaris'
subscribers_count: '{0}Subscriptores|{1}Subscriptora|]1,Inf[ Subscriptores'
followers_count: '{0}Seguidores|{1}Seguidora|]1,Inf[ Seguidores'
marked_for_deletion: Marcat per a supressió
marked_for_deletion_at: Marcat per suprimir-se el %date%
favourites: Vots a favor
favourite: Preferit
avatar: Avatar
added: Afegit
down_votes: Vots en contra
up_votes: Impulsos
no_comments: Sense comentaris
created_at: Creat
owner: Propietari(a)
top: Destacat
active: Actiu(va)
more: Més
login: Iniciar sessió
subscribers: Subscritors(es)
online: En línia
comments: Comentaris
posts: Publicacions
replies: Respostes
moderators: Moderació
add_comment: Afegir comentari
add_post: Afegir publicació
add_media: Afegir mitjà
remove_user_avatar: Eliminar avatar
mod_log: Registre de moderació
remove_media: Eliminar mitjà
disconnected_magazine_info: 'Aquesta revista no està rebent actualitzacions: darrera
activitat fa %days% dia(es).'
activity: Activitat
markdown_howto: Com funciona l'editor?
enter_your_comment: Escriviu el comentari
enter_your_post: Escriviu la publicació
cover: Portada
remove_user_cover: Eliminar portada
related_posts: Publicacions relacionades
random_posts: Publicacions aleatòries
federated_magazine_info: Aquesta revista és d'un servidor federat i pot estar
incompleta.
always_disconnected_magazine_info: Aquesta revista no està rebent
actualitzacions.
federated_user_info: Aquest perfil és d'un servidor federat i pot ser incomplet.
subscribe_for_updates: Subscriviu-vos per començar a rebre actualitzacions.
contact: Contacte
already_have_account: Ja teniu un compte?
from: des de
links: Enllaços
you_cant_login: Heu oblidat la contrasenya?
enabled: Habilitat
followers: Seguidor(e)s
reset_check_email_desc: Si ja hi ha un compte associat a la vostra adreça
electrònica, rebreu un correu electrònic en breu amb un enllaç que podeu
utilitzar per restablir la vostra contrasenya. Aquest enllaç caducarà en
%expire%.
email_confirm_expire: Tingueu en compte que l'enllaç caducarà d'aquí a una hora.
moderated: Moderat(da)
tag: Etiqueta
eng: ENG
columns: Columnes
chat_view: Vista de xat
photos: Fotos
are_you_sure: Ho confirmeu?
go_to_search: Anar a la cerca
menu: Menú
blocked: Blocats
domain: Domini
subscriptions: Subscripcions
overview: Vista general
people_local: Local
people_federated: Federat
subscribed: Subscrit(a)
reason: Motiu
homepage: Pàgina d'inici
copy_url_to_fediverse: Copiar URL original
edit_entry: Editar fil
delete: Eliminar
edit_post: Editar publicació
show_profile_followings: Mostrar els comptes seguits
appearance: Aparença
6h: 6h
3h: 3h
go_to_filters: Anar a filtres
tree_view: Vista d'arbre
12h: 12h
articles: Fils
report: Denunciar
all: Tot
1d: 1 dia
show_profile_subscriptions: Mostrar subscripcions a revistes
notify_on_new_entry: Fils nous (enllaços o articles) a qualsevol revista a què
estic subscrit
table_view: Vista de taula
general: General
profile: Perfil
cards_view: Vista de targetes
email_verify: Confirmeu l'adreça electrònica
following: Seguint
rss: RSS
1y: 1 any
videos: Vídeos
share_on_fediverse: Compartir al Fedivers
edit_comment: Desar canvis
messages: Missatges
edit: Editar
moderate: Moderar
1m: 1 mes
notify_on_new_post_reply: Qualsevol nivell de respostes a les publicacions que
he escrit
copy_url: Copiar URL de Mbin
settings: Configuració
notify_on_new_posts: Noves publicacions a qualsevol revista a què estic subscrit
reports: Denúncies
notifications: Notificacions
agree_terms: Accepteu els %terms_link_start%Termes i condicions%terms_link_end%
i la %policy_link_start%Política de privadesa%policy_link_end%
go_to_original_instance: Veure en instància remota
empty: Buit
subscribe: Subscriure's
unsubscribe: Cancel·lar subscripció
follow: Seguir
unfollow: Deixar de seguir
reply: Resposta
login_or_email: Identificador o adreça electrònica
password: Contrasenya
dont_have_account: No teniu un compte?
remember_me: Recordar-me
register: Crear compte
reset_password: Restablir contrasenya
show_more: Mostrar més
to: a
in: en
username: Identificador
email: Adreça electrònica
repeat_password: Repetiu la contrasenya
terms: Condicions del servei
privacy_policy: Política de privadesa
about_instance: Quant a
all_magazines: Totes les revistes
stats: Estadístiques
fediverse: Fedivers
create_new_magazine: Crear revista nova
add_new_article: Afegir fil nou
add_new_link: Afegir enllaç nou
add_new_photo: Afegir foto nova
add_new_post: Afegir publicació nova
add_new_video: Afegir vídeo nou
change_theme: Canviar tema
downvotes_mode: Mode de vots negatius
change_downvotes_mode: Canviar el mode de vots en contra
disabled: Deshabilitat
hidden: Amagat
faq: Preguntes més freqüents (PMF)
useful: Útil
help: Ajuda
check_email: Comproveu la vostra bústia electrònica
reset_check_email_desc2: Si no rebeu cap correu electrònic, comproveu la vostra
carpeta de correu brossa.
try_again: Torneu-ho a provar
up_vote: Impulsar
down_vote: Votar en contra
email_confirm_header: Hola! Confirmeu la vostra adreça electrònica.
email_confirm_content: "Per activar el compte de Mbin feu clic a l'enllaç següent:"
email_confirm_title: Confirmeu la vostra adreça electrònica.
select_magazine: Trieu una revista
add_new: Afegir nou
url: URL
title: Títol
body: Cos
tags: Etiquetes
badges: Insígnies
is_adult: +18 / Explícit
oc: Cont. Orig.
image: Imatge
image_alt: Text alternatiu de la imatge
name: Nom
description: Descripció
rules: Normes
cards: Targetes
user: Usuari(a)
joined: Inscrit(a)
reputation_points: Punts de reputació
related_tags: Etiquetes relacionades
go_to_content: Anar al contingut
logout: Tancar sessió
classic_view: Vista clàssica
compact_view: Vista compacta
1w: 1 setm.
share: Compartir
hide_adult: Amagar contingut explícit
featured_magazines: Revistes destacades
privacy: Privadesa
notify_on_new_entry_reply: Qualsevol nivell de comentaris als fils que he escrit
notify_on_new_entry_comment_reply: Respostes als meus comentaris en qualsevol
fil
notify_on_new_post_comment_reply: Respostes als meus comentaris a qualsevol
publicació
notify_on_user_signup: Nous registres
save: Desar
about: Quant a
old_email: Adreça electrònica actual
new_email: Nova adreça electrònica
new_email_repeat: Confirmar l'adreça electrònica nova
current_password: Contrasenya actual
new_password: Contrasenya nova
new_password_repeat: Confirmar la nova contrasenya
change_email: Canviar l'adreça electrònica
change_password: Canviar la contrasenya
expand: Desplegar
domains: Dominis
votes: Vots
theme: Tema
dark: Fosc
light: Clar
solarized_light: Clar solaritzat
solarized_dark: Fosc solaritzat
default_theme: Tema predeterminat
default_theme_auto: Clar/fosc (detecció automàtica)
solarized_auto: Solaritzat (detecció automàtica)
font_size: Mida de la lletra
boosts: Impulsos
show_users_avatars: Mostrar avatars d'usuaris(es)
show_thumbnails: Mostrar miniatures
show_magazines_icons: Mostrar icones de les revistes
rounded_edges: Vores arrodonides
removed_thread_by: ha eliminat un fil de
restored_thread_by: ha restaurat un fil de
restored_comment_by: ha restaurat el comentari de
removed_post_by: ha eliminat una publicació de
restored_post_by: ha restaurat una publicació de
he_banned: bandejat(da)
he_unbanned: desbandejat(da)
read_all: Marcar-ho tot com a llegit
show_all: Mostrar-ho tot
flash_thread_edit_success: El fil s'ha editat correctament.
flash_magazine_edit_success: La revista ha estat editada amb èxit.
flash_unmark_as_adult_success: La publicació s'ha desmarcat correctament com a
explícita.
flash_mark_as_adult_success: La publicació s'ha marcat correctament com a
explícita.
too_many_requests: S'ha superat el límit; torneu-ho a provar més tard.
set_magazines_bar: Barra de revistes
set_magazines_bar_desc: afegiu els noms de les revistes després de la coma
set_magazines_bar_empty_desc: si el camp està buit, les revistes actives es
mostren a la barra.
added_new_thread: S'ha afegit un fil nou
edited_thread: Ha editat un fil
mod_log_alert: 'ADVERTÈNCIA: En el registre de moderació podreu trobar contingut desagradable
o ofensiu eliminat per la moderació. Assegureu-vos de saber el que esteu fent.'
replied_to_your_comment: Ha respost al vostre comentari
added_new_post: Ha afegit una publicació nova
edited_post: Ha editat una publicació
edited_comment: Ha editat un comentari
added_new_reply: Ha afegit una nova resposta
mod_deleted_your_comment: La moderació ha suprimit el vostre comentari
mod_remove_your_post: La moderació ha eliminat la vostra publicació
no: No
error: Error
collapse: Plegar
flash_register_success: Benvinguda a bord! El vostre compte ja està registrat.
Un últim pas - consulteu la vostra safata d'entrada per a rebre un enllaç
d'activació que donarà vida al vostre compte.
flash_thread_new_success: El fil s'ha creat correctament i ara és visible per a
altres usuaris(es).
flash_thread_unpin_success: El fil s'ha desfixat correctament.
yes: Sí
size: Mida
removed_comment_by: ha eliminat un comentari de
flash_thread_delete_success: El fil s'ha suprimit correctament.
flash_thread_pin_success: El fil s'ha fixat correctament.
flash_magazine_new_success: La revista ha estat creada amb èxit. Ara podeu
afegir contingut nou o explorar el tauler d'administració de la revista.
mod_remove_your_thread: La moderació ha eliminat el vostre fil
added_new_comment: Ha afegit un comentari nou
wrote_message: Ha escrit un missatge
banned: Us ha bandejat
removed: Eliminat per la moderació
deleted: Esborrat per l'autor
mentioned_you: Us ha esmentat
comment: Comentari
post: Publicació
ban_expired: El bandeig ha expirat
purge: Buidar la llista
send_message: Enviar missatge directe
sticky_navbar: Barra de navegació fixa
subject_reported: S'ha denunciat el contingut.
sidebar_position: Posició de la barra lateral
left: Esquerra
right: Dreta
federation: Federació
status: Estat
on: Encès
off: Apagat
instances: Instàncies
from_url: Des de l'URL
magazine_panel: Panell de la revista
reject: Rebutjar
approve: Aprovar
ban: Bandejar
unban: Desbandejar
unban_hashtag_btn: Desbandejar hashtag
ban_hashtag_btn: Bandejar hashtag
filters: Filtres
approved: Aprovat
rejected: Rebutjat
add_moderator: Afegir moderador(a)
add_badge: Afegir insígnia
bans: Bandejos
created: Creat
expires: Caduca
perm: Permanent
expired_at: Caducà el
add_ban: Afegir bandeig
trash: Paperera
icon: Icona
done: Fet
pin: Fixar
unpin: Desfixar
change_language: Canviar idioma
mark_as_adult: Marcar com a explícit
unmark_as_adult: Desmarcar com a explícit
change: Canviar
pinned: Fixat
preview: Previsualitzar
article: Fil
reputation: Reputació
note: Nota
writing: Escriptura
users: Usuaris(es)
content: Contingut
week: Setmana
weeks: Setmanes
month: Mes
months: Mesos
federated: Federat
local: Local
admin_panel: Tauler d'administració
dashboard: Tauler de control
contact_email: Adreça electrònica de contacte
instance: Instància
pages: Pàgines
FAQ: Preguntes més freqüents (PMF)
type_search_term: Escriviu el terme de cerca
registration_disabled: Registre desactivat
restore: Restaurar
add_mentions_entries: Afegir etiquetes de menció als fils
add_mentions_posts: Afegir etiquetes de menció a les publicacions
Password is invalid: La contrasenya no és vàlida.
Your account is not active: El vostre compte no està actiu.
Your account has been banned: El vostre compte ha estat bandejat.
firstname: Nom
send: Enviar
active_users: Persones actives
related_entries: Fils relacionats
purge_account: Purgar el compte
ban_account: Bandejar el compte
unban_account: Desbandejar el compte
related_magazines: Revistes relacionades
random_magazines: Revistes aleatòries
sidebar: Barra lateral
auto_preview: Vista prèvia automàtica dels mitjans
dynamic_lists: Llistes dinàmiques
banned_instances: Instàncies bandejades
kbin_intro_title: Explorar el fedivers
kbin_promo_title: Creeu la vostra pròpia instància
kbin_promo_desc: '%link_start%Cloneu el repositori%link_end% i desenvolupeu fedivers'
captcha_enabled: Captcha activat
header_logo: Logotip de la capçalera
browsing_one_thread: Només esteu navegant per un fil de la discussió! Tots els
comentaris estan disponibles a la pàgina de publicació.
viewing_one_signup_request: Només esteu veient una sol·licitud de registre de
%username%
return: Tornar
boost: Impulsar
mercure_enabled: Mercure activat
report_issue: Denunciar problema
tokyo_night: Nit de Tòquio
preferred_languages: Filtrar els idiomes de fils i publicacions
infinite_scroll_help: Carregar automàticament més contingut en arribar a la part
inferior de la pàgina.
auto_preview_help: Mostrar les previsualitzacions multimèdia (foto, vídeo) en
una mida més gran a sota del contingut.
reload_to_apply: Torneu a carregar la pàgina per aplicar els canvis
filter.origin.label: Trieu l'origen
filter.fields.label: Trieu quins camps voleu cercar
filter.adult.label: Trieu si voleu mostrar contingut explícit
filter.adult.hide: Amagar contingut explícit
filter.adult.only: Només el contingut explícit
local_and_federated: Local i federat
filter.fields.only_names: Només noms
filter.fields.names_and_descriptions: Noms i descripcions
kbin_bot: Agent Mbin
password_confirm_header: Confirmeu la vostra sol·licitud de canvi de
contrasenya.
your_account_is_not_active: El vostre compte no s'ha activat. Comproveu la
vostra bústia electrònica per obtenir instruccions d'activació del compte o sol·liciteu un correu electrònic d'activació del compte
nou.
your_account_has_been_banned: El vostre compte ha estat bandejat
your_account_is_not_yet_approved: El vostre compte encara no s'ha aprovat.
Enviarem un correu electrònic tan bon punt l'administració hagi processat la
vostra sol·licitud de registre.
toolbar.bold: Negreta
toolbar.italic: Itàlica
toolbar.strikethrough: Ratllat
toolbar.header: Capçalera
toolbar.quote: Cita
toolbar.code: Codi
toolbar.link: Enllaç
toolbar.image: Imatge
toolbar.ordered_list: Llista ordenada
toolbar.mention: Esment
toolbar.spoiler: Spoiler
year: Any
upload_file: Pujar arxiu
magazine_panel_tags_info: Indiqueu-ho només si voleu que el contingut del
fedivers s'inclogui en aquesta revista segons les etiquetes
registrations_enabled: Registre activat
message: Missatge
toolbar.unordered_list: Llista no ordenada
infinite_scroll: Desplaçament infinit
filter.adult.show: Mostrar el contingut explícit
show_top_bar: Mostrar barra superior
ban_hashtag_description: Bandejar un hashtag impedirà que es creïn publicacions
amb aquest hashtag, a més d'amagar les publicacions existents amb aquest
hashtag.
unban_hashtag_description: Desbandejar un hashtag permetrà tornar a crear
publicacions amb aquest hashtag. Les publicacions existents amb aquest hashtag
ja no s'amaguen.
change_magazine: Canviar revista
federation_enabled: Federació activada
sticky_navbar_help: La barra de navegació es fixarà a la part superior de la
pàgina quan us desplaceu cap avall.
meta: Meta
random_entries: Fils aleatoris
kbin_intro_desc: és una plataforma descentralitzada per a l'agregació de
continguts i microblogging que opera dins de la xarxa fedivers.
bot_body_content: "Benvinguda a l'agent Mbin! Aquest agent té un paper crucial per
habilitar la funcionalitat d'ActivityPub dins de Mbin. Assegura que Mbin es pugui
comunicar i federar amb altres instàncies del fedivers.\n\nActivityPub és un protocol
estàndard obert que permet que les plataformes de xarxes socials descentralitzades
es comuniquin i interactuïn entre elles. Permet a usuari(e)s de diferents instàncies
(servidors) seguir, interactuar i compartir contingut a través de la xarxa social
federada coneguda com a fedivers. Proporciona una manera estandarditzada per publicar
contingut, seguir altres usuaris(es) i participar en interaccions socials, com ara
fer m'agrada, compartir i comentar fils o publicacions."
delete_account: Suprimir el compte
federation_page_enabled: Pàgina de federació activada
federation_page_allowed_description: Instàncies conegudes amb què ens federem
federation_page_disallowed_description: Instàncies amb què no ens federem
federation_page_dead_title: Instàncies mortes
federated_search_only_loggedin: Cerca federada limitada si no s'ha iniciat
sessió
account_deletion_title: Supressió del compte
account_deletion_button: Suprimir el compte
account_deletion_immediate: Suprimir immediatament
errors.server500.title: 500 Error intern del servidor
errors.server429.title: 429 Massa sol·licituds
errors.server404.title: 404 No trobat
email.delete.description: L'usuari(a) següent ha sol·licitat que s'elimini el
seu compte
resend_account_activation_email_question: Compte inactiu?
resend_account_activation_email_success: Si existeix un compte associat amb
l'adreça electrònica, hi enviarem un nou correu d'activació.
resend_account_activation_email_description: Introduïu l'adreça electrònica
associada al vostre compte. Us hi enviarem un altre correu d'activació.
custom_css: CSS personalitzat
ignore_magazines_custom_css: Ignorar el CSS personalitzat de les revistes
oauth.consent.title: Formulari de consentiment OAuth2
oauth.consent.grant_permissions: Concedir permisos
oauth.consent.app_requesting_permissions: voldria realitzar les accions següents
en nom vostre
oauth.consent.app_has_permissions: ja pot realitzar les accions següents
oauth.consent.allow: Permetre
oauth.consent.deny: Denegar
oauth.client_identifier.invalid: Identificador de client OAuth no vàlid!
oauth.client_not_granted_message_read_permission: Aquesta aplicació no ha rebut
permís per llegir els vostres missatges.
restrict_oauth_clients: Restringir la creació de clients OAuth2 a
l'administració
block: Blocar
unblock: Desblocar
oauth2.grant.moderate.magazine.list: Mostrar la llista de les revistes que
modereu.
oauth2.grant.moderate.magazine.reports.read: Mostrar les denúncies a les
revistes que modereu.
oauth2.grant.moderate.magazine.reports.action: Acceptar o rebutjar denúncies a
les revistes que modereu.
oauth2.grant.moderate.magazine.trash.read: Veure el contingut a la paperera de
les revistes que modereu.
oauth2.grant.moderate.magazine_admin.create: Crear noves revistes.
oauth2.grant.moderate.magazine_admin.delete: Suprimir qualsevol de les vostres
revistes.
oauth2.grant.moderate.magazine_admin.update: Editar les regles, la descripció,
el mode explícit o la icona de les vostres revistes.
oauth2.grant.moderate.magazine_admin.moderators: Afegir o eliminar moderador(e)s
de qualsevol de les vostres revistes.
oauth2.grant.moderate.magazine_admin.badges: Crear o eliminar insígnies de les
vostres revistes.
oauth2.grant.moderate.magazine_admin.tags: Crear o eliminar etiquetes de les
vostres revistes.
oauth2.grant.moderate.magazine_admin.stats: Veure el contingut, votar i
consultar les estadístiques de les vostres revistes.
oauth2.grant.admin.all: Realitzar qualsevol acció administrativa sobre la vostra
instància.
oauth2.grant.read.general: Llegir tot el contingut a què tingueu accés.
oauth2.grant.write.general: Crear o editar qualsevol dels vostres fils,
publicacions o comentaris.
oauth2.grant.delete.general: Suprimir qualsevol dels vostres fils, publicacions
o comentaris.
oauth2.grant.report.general: Denunciar fils, publicacions o comentaris.
oauth2.grant.block.general: Blocar o desblocar qualsevol revista, domini o
compte i veure les revistes, dominis i comptes que heu blocat.
oauth2.grant.domain.subscribe: Subscriure-vos o cancel·lar la subscripció als
dominis i veure els dominis a què us subscriviu.
oauth2.grant.domain.block: Blocar o desblocar dominis i veure els dominis que
heu blocat.
oauth2.grant.entry.all: Crear, editar o suprimir els vostres fils i votar,
impulsar o denunciar qualsevol fil.
oauth2.grant.entry.create: Crear fils nous.
oauth2.grant.entry.edit: Editar els vostres fils existents.
oauth2.grant.entry.delete: Suprimir els vostres fils existents.
oauth2.grant.entry.vote: Votar a favor, impulsar o votar en contra de qualsevol
fil.
oauth2.grant.entry.report: Denunciar qualsevol fil.
oauth2.grant.entry_comment.create: Crear comentaris nous en fils.
oauth2.grant.entry_comment.edit: Editar els vostres comentaris existents als
fils.
oauth2.grant.entry_comment.delete: Suprimir els vostres comentaris existents als
fils.
oauth2.grant.entry_comment.report: Denunciar qualsevol comentari en un fil.
oauth2.grant.magazine.subscribe: Subscriure-vos o cancel·lar la subscripció a
revistes i veure les revistes a què us subscriviu.
oauth2.grant.magazine.block: Blocar o desblocar revistes i veure les revistes
que heu blocat.
oauth2.grant.post.all: Crear, editar o suprimir els vostres microblogs i votar,
impulsar o denunciar qualsevol microblog.
oauth2.grant.post.edit: Editar les vostres publicacions existents.
oauth2.grant.post.delete: Suprimir les vostres publicacions existents.
oauth2.grant.post.vote: Votar a favor, impulsar o votar en contra de qualsevol
publicació.
oauth2.grant.post_comment.all: Crear, editar o suprimir els vostres comentaris a
les publicacions i votar, impulsar o denunciar qualsevol comentari en una
publicació.
oauth2.grant.post_comment.edit: Editar els vostres comentaris existents a les
publicacions.
oauth2.grant.post_comment.delete: Suprimir els vostres comentaris existents a
les publicacions.
oauth2.grant.post_comment.report: Denunciar qualsevol comentari en una
publicació.
oauth2.grant.user.bookmark: Afegir i eliminar marcadors
oauth2.grant.user.bookmark.add: Afegir marcadors
oauth2.grant.user.bookmark.remove: Eliminar marcadors
oauth2.grant.user.bookmark_list: Veure, editar i suprimir les vostres llistes de
marcadors
oauth2.grant.user.bookmark_list.read: Veure les vostres llistes de marcadors
oauth2.grant.user.bookmark_list.edit: Editar les vostres llistes de marcadors
oauth2.grant.user.bookmark_list.delete: Esborrar les vostres llistes de
marcadors
oauth2.grant.user.profile.all: Llegir i editar el vostre perfil.
oauth2.grant.user.message.all: Llegir els vostres missatges i enviar missatges a
altres usuaris(es).
oauth2.grant.user.message.read: Llegir els vostres missatges.
oauth2.grant.user.message.create: Enviar missatges a altres usuaris(es).
oauth2.grant.user.notification.read: Llegir les vostres notificacions, incloses
les notificacions de missatges.
oauth2.grant.user.oauth_clients.read: Veure els permisos que heu concedit a
altres aplicacions OAuth2.
oauth2.grant.user.oauth_clients.edit: Editar els permisos que heu concedit a
altres aplicacions OAuth2.
oauth2.grant.user.block: Blocar o desblocar comptes i veure una llista de
comptes que bloqueu.
oauth2.grant.moderate.all: Realitzar qualsevol acció de moderació que tingueu
permís per dur a terme a les revistes que modereu.
oauth2.grant.moderate.entry.pin: Fixar els fils a la part superior de les
revistes que modereu.
oauth2.grant.moderate.entry.set_adult: Marcar els fils com a explícits a les
revistes que modereu.
oauth2.grant.moderate.entry.trash: Ficar a la paperera o restaurar fils a les
revistes que modereu.
oauth2.grant.moderate.entry_comment.all: Moderar els comentaris als fils de les
revistes que modereu.
oauth2.grant.moderate.entry_comment.change_language: Canviar l'idioma dels
comentaris als fils de les revistes que modereu.
oauth2.grant.moderate.entry_comment.set_adult: Marcar els comentaris als fils
com a explícits a les revistes que modereu.
oauth2.grant.moderate.entry_comment.trash: Ficar a la paperera o restaurar els
comentaris dels fils de les revistes que modereu.
oauth2.grant.moderate.post.change_language: Canviar l'idioma de les publicacions
a les revistes que modereu.
oauth2.grant.moderate.post.trash: Ficar a la paperera o restaurar les
publicacions de les revistes que modereu.
oauth2.grant.moderate.post_comment.all: Moderar els comentaris a les
publicacions de les revistes que modereu.
oauth2.grant.moderate.post_comment.set_adult: Marcar com a explícits els
comentaris de les revistes que modereu.
oauth2.grant.moderate.post_comment.trash: Ficar a la paperera o restaurar els
comentaris a les publicacions de les revistes que modereu.
oauth2.grant.moderate.magazine.all: Gestionar els bandejos, les denúncies i
veure els articles a la paperera a les revistes que modereu.
oauth2.grant.moderate.magazine.ban.read: Veure els comptes bandejats a les
revistes que modereu.
oauth2.grant.moderate.magazine.ban.create: Bandejar comptes a les revistes que
modereu.
oauth2.grant.admin.entry_comment.purge: Suprimir completament qualsevol
comentari d'un fil de la vostra instància.
oauth2.grant.admin.post_comment.purge: Suprimir completament qualsevol comentari
d'una publicació de la vostra instància.
oauth2.grant.admin.magazine.move_entry: Moure fils entre revistes a la vostra
instància.
oauth2.grant.admin.magazine.purge: Suprimir completament les revistes de la
vostra instància.
oauth2.grant.admin.user.verify: Verificar usuaris(es) a la vostra instància.
oauth2.grant.admin.user.delete: Eliminar comptes de la vostra instància.
oauth2.grant.admin.user.purge: Eliminar completament comptes de la vostra
instància.
oauth2.grant.admin.instance.all: Veure i actualitzar la configuració o la
informació de la instància.
oauth2.grant.admin.instance.stats: Veure les estadístiques de la vostra
instància.
oauth2.grant.admin.instance.settings.read: Veure la configuració de la vostra
instància.
oauth2.grant.admin.instance.settings.edit: Actualitzar la configuració de la
vostra instància.
oauth2.grant.admin.federation.all: Veure i actualitzar les instàncies
desfederades actualment.
oauth2.grant.admin.federation.read: Veure la llista d'instàncies desfederades.
oauth2.grant.admin.federation.update: Afegir o eliminar instàncies de la llista
d'instàncies desfederades.
oauth2.grant.admin.oauth_clients.all: Veure o revocar els clients OAuth2 que
existeixen a la vostra instància.
oauth2.grant.admin.oauth_clients.read: Veure els clients OAuth2 que existeixen a
la vostra instància i les seves estadístiques d'ús.
oauth2.grant.admin.oauth_clients.revoke: Revocar l'accés als clients OAuth2 a la
vostra instància.
last_active: Última activitat
flash_post_pin_success: La publicació s'ha fixat correctament.
flash_post_unpin_success: La publicació s'ha desfixat correctament.
show_avatars_on_comments: Mostrar avatars als comentaris
single_settings: Únic
update_comment: Actualitzar comentari
show_avatars_on_comments_help: Mostrar/amagar els avatars quan es veuen
comentaris en un sol fil o publicació.
comment_reply_position: Posició del comentari de resposta
magazine_theme_appearance_icon: Icona personalitzada per a la revista.
magazine_theme_appearance_background_image: Imatge de fons personalitzada que
s'aplicarà quan visualitzeu contingut a la vostra revista.
moderation.report.approve_report_title: Aprovar la denúncia
moderation.report.reject_report_title: Rebutjar la denúncia
subject_reported_exists: Aquest contingut ja s'ha denunciat.
moderation.report.ban_user_title: Bandejar compte
moderation.report.reject_report_confirmation: Confirmeu que voleu rebutjar
aquesta denúncia?
delete_content: Suprimir contingut
purge_content: Purgar contingut
delete_content_desc: Suprimir el contingut de l'usuari(a) deixant les respostes
d'altres usuaris(es) als fils, publicacions i comentaris creats.
schedule_delete_account: Programar eliminació
schedule_delete_account_desc: Programar la supressió d'aquest compte en 30 dies.
Això amagarà l'usuari(a) i el seu contingut, i també impedirà que iniciï
sessió.
remove_schedule_delete_account: Cancel·lar la supressió programada
remove_schedule_delete_account_desc: Cancel·lar la programació de l'eliminació.
Tot el contingut tornarà a estar disponible i el compte podrà iniciar sessió.
two_factor_backup: Codis de recolzament d'autenticació de dos factors
2fa.authentication_code.label: Codi d'autenticació
2fa.verify: Verificar
2fa.code_invalid: El codi d'autenticació no és vàlid
2fa.enable: Configurar l'autenticació de dos factors
2fa.backup-create.label: Crear codis d'autenticació de recolzament nous
2fa.remove: Eliminar l'autenticació de dos factors
2fa.add: Afegir al meu compte
2fa.verify_authentication_code.label: Introduïu un codi de dos factors per
verificar la configuració
2fa.qr_code_img.alt: Un codi QR que permet configurar l'autenticació de dos
factors per al vostre compte
2fa.qr_code_link.title: En visitar aquest enllaç permetreu a la vostra
plataforma registrar aquesta autenticació de dos factors
2fa.user_active_tfa.title: L'usuari(a) té actiu el doble factor d'autenticació
cancel: Cancel·lar
2fa.backup_codes.recommendation: Guardeu-ne una còpia en un lloc segur.
password_and_2fa: Contrasenya i A2F
show_subscriptions: Mostrar subscripcions
subscription_sort: Ordenar
alphabetically: Alfabèticament
subscriptions_in_own_sidebar: A una barra lateral separada
sidebars_same_side: Barres laterals al mateix costat
subscription_sidebar_pop_out_right: Moure a la barra lateral separada de la
dreta
subscription_sidebar_pop_out_left: Moure a la barra lateral separada de
l'esquerra
subscription_panel_large: Panell gran
close: Tancar
position_bottom: Inferior
position_top: Superior
pending: Pendent
flash_thread_new_error: No s'ha pogut crear el fil. Alguna cosa ha fallat.
flash_thread_tag_banned_error: No s'ha pogut crear el fil. El contingut no està
permès.
flash_image_download_too_large_error: La imatge no s'ha pogut crear, és massa
gran (mida màxima %bytes%)
flash_email_was_sent: El correu electrònic s'ha enviat correctament.
flash_email_failed_to_sent: No s'ha pogut enviar el correu electrònic.
flash_post_new_success: La publicació s'ha creat correctament.
flash_magazine_theme_changed_success: S'ha actualitzat correctament l'aparença
de la revista.
flash_magazine_theme_changed_error: No s'ha pogut actualitzar l'aparença de la
revista.
flash_comment_edit_success: El comentari s'ha actualitzat correctament.
flash_comment_edit_error: No s'ha pogut editar el comentari. Alguna cosa ha
fallat.
flash_user_settings_general_error: No s'ha pogut desar la configuració
d'usuari(a).
flash_user_edit_profile_error: No s'ha pogut desar la configuració del perfil.
flash_user_edit_password_error: No s'ha pogut canviar la contrasenya.
flash_thread_edit_error: No s'ha pogut editar el fil. Alguna cosa ha fallat.
flash_post_edit_error: No s'ha pogut editar la publicació. Alguna cosa ha
fallat.
flash_post_edit_success: La publicació s'ha editat correctament.
page_width: Amplada de pàgina
page_width_max: Màxim
page_width_auto: Automàtic
page_width_fixed: Fix
filter_labels: Filtrar etiquetes
auto: Automàtic
change_my_avatar: Canviar el meu avatar
change_my_cover: Canviar la meva portada
edit_my_profile: Editar el meu perfil
account_settings_changed: La configuració del vostre compte s'ha canviat
correctament. Haureu de tornar a iniciar sessió.
magazine_deletion: Eliminació de la revista
delete_magazine: Suprimir revista
restore_magazine: Recuperar revista
purge_magazine: Purgar revista
magazine_is_deleted: S'ha suprimit la revista. Podeu recuperar-la en un termini de 30 dies.
user_suspend_desc: En suspendre el compte s'amaga el contingut a la instància,
però no l'elimina permanentment i el podeu restaurar en qualsevol moment.
account_banned: El compte ha estat bandejat.
remove_subscriptions: Eliminar subscripcions
apply_for_moderator: Sol·licitar ser moderador(a)
request_magazine_ownership: Demanar la propietat de la revista
cancel_request: Cancel·lar sol·licitud
abandoned: Abandonat
ownership_requests: Sol·licituds de propietat
accept: Acceptar
moderator_requests: Sol·licituds de moderació
action: Acció
user_badge_op: OP
user_badge_admin: Administrador(a)
user_badge_global_moderator: Moderador(a) global
user_badge_moderator: Moderador(a)
user_badge_bot: Bot
announcement: Anunci
keywords: Paraules clau
deleted_by_author: L'autor(a) ha eliminat el fil, la publicació o el comentari
sensitive_warning: Contingut sensible
sensitive_toggle: Commutar la visibilitat del contingut sensible
sensitive_show: Feu clic per mostrar
sensitive_hide: Feu clic per amagar
details: Detalls
spoiler: Spoiler
all_time: Tot el temps
show: Mostrar
hide: Amagar
edited: editat
sso_registrations_enabled: Registres SSO activats
continue_with: Continuar amb
own_report_accepted: La vostra denúncia ha estat acceptada
report_accepted: S'ha acceptat una denúncia
magazine_log_mod_added: ha afegit un(a) moderador(a)
magazine_log_mod_removed: ha llevat un(a) moderador(a)
magazine_log_entry_pinned: entrada fixada
magazine_log_entry_unpinned: s'ha eliminat l'entrada fixada
last_updated: Última actualització
and: i
direct_message: Missatge directe
manually_approves_followers: Aprova seguidors(es) manualment
register_push_notifications_button: Registreu-vos per a les notificacions «push»
unregister_push_notifications_button: Eliminar el registre de «push»
test_push_notifications_button: Provar les notificacions «push»
test_push_message: Hola món!
notification_title_new_comment: Nou comentari
notification_title_removed_comment: S'ha eliminat un comentari
notification_title_edited_comment: S'ha editat un comentari
notification_title_new_reply: Nova resposta
notification_title_new_thread: Nou fil
notification_title_removed_thread: S'ha eliminat un fil
notification_title_edited_thread: S'ha editat un fil
notification_title_ban: Us han bandejat
notification_title_message: Nou missatge directe
notification_title_new_post: Publicació nova
notification_title_removed_post: S'ha eliminat una publicació
notification_title_edited_post: S'ha editat una publicació
notification_title_new_signup: S'ha registrat un nou compte
notification_body_new_signup: S'ha registrat el compte %u%.
notification_body2_new_signup_approval: Heu d'aprovar la sol·licitud abans que
puguin iniciar sessió
show_related_magazines: Mostrar revistes aleatòries
show_related_entries: Mostrar fils aleatoris
show_related_posts: Mostrar publicacions aleatòries
show_active_users: Mostrar comptes actius
notification_title_new_report: S'ha creat una nova denúncia
magazine_posting_restricted_to_mods_warning: Només la moderació pot crear fils
en aquesta revista
flash_posting_restricted_error: La creació de fils està restringida a la
moderació d'aquesta revista i no en sou part
server_software: Programari del servidor
version: Versió
last_successful_deliver: Darrera entrega correcta
last_successful_receive: Darrera rebuda correcta
last_failed_contact: Últim contacte fallit
new_user_description: Aquest compte és nou (actiu durant menys de %days% dies)
new_magazine_description: Aquesta revista és nova (activa durant menys de %days%
dies)
admin_users_suspended: Suspesos(es)
admin_users_active: Actius(ves)
admin_users_banned: Bandejats(des)
user_verify: Activar compte
max_image_size: Mida màxima del fitxer
comment_not_found: No s'ha trobat el comentari
bookmark_remove_from_list: Eliminar el marcador de %list%
bookmark_add_to_list: Afegir marcador a %list%
bookmark_remove_all: Eliminar tots els marcadors
bookmark_add_to_default_list: Afegir marcador a la llista predeterminada
bookmark_lists: Llistes de marcadors
bookmarks: Marcadors
bookmarks_list: Marcadors en %list%
count: Recompte
is_default: És predeterminada
bookmark_list_make_default: Fer predeterminada
bookmark_list_is_default: És la llista predeterminada
bookmark_list_create: Crear
bookmark_list_create_placeholder: escriviu el nom…
bookmark_list_create_label: Nom de la llista
bookmarks_list_edit: Editar la llista de marcadors
errors.server403.title: 403 Prohibit
resend_account_activation_email: Tornar a enviar el correu electrònic
d'activació del compte
federation_page_dead_description: Instàncies en què no vam poder lliurar almenys
10 activitats seguides i on l'última entrega i recepció amb èxit van ser fa
més d'una setmana
oauth2.grant.moderate.magazine.ban.delete: Desblocar usuaris(es) de les revistes
que modereu.
oauth2.grant.domain.all: Subscriure-vos o blocar dominis i veure els dominis a
què us subscriviu o que bloqueu.
account_deletion_description: El vostre compte se suprimirà d'aquí a 30 dies
tret que decidiu suprimir-lo immediatament. Per restaurar el vostre compte en
un termini de 30 dies, inicieu sessió amb les mateixes credencials o poseu-vos
en contacte amb l'equip d'administració.
oauth2.grant.moderate.magazine_admin.edit_theme: Editar el CSS personalitzat de
qualsevol de les vostres revistes.
more_from_domain: Més del domini
email_confirm_button_text: Confirmeu la vostra sol·licitud de canvi de
contrasenya
errors.server500.description: Ho sentim, hi ha hagut un error al nostre costat.
Si continueu veient aquest error, proveu de contactar amb l'administració de
la instància. Si aquesta instància no funciona en absolut, aneu a
%link_start%altres instàncies de Mbin%link_end% mentrestant fins que es
resolgui el problema.
resend_account_activation_email_error: Hi ha hagut un problema en enviar la
sol·licitud. Potser no hi ha cap compte associat amb l'adreça electrònica o
potser ja està activat.
email_confirm_link_help: Alternativament, podeu copiar i enganxar el següent al
vostre navegador
email.delete.title: Sol·licitud d'eliminació del compte
oauth2.grant.moderate.magazine.reports.all: Gestionar les denúncies a les
revistes que modereu.
oauth.consent.to_allow_access: Per permetre aquest accés feu clic al botó
«Permetre” a continuació
private_instance: Forçar a iniciar sessió abans de poder accedir a qualsevol
contingut
oauth2.grant.moderate.magazine_admin.all: Crear, editar o suprimir les vostres
revistes.
oauth2.grant.subscribe.general: Subscriure-vos o seguir qualsevol revista,
domini o compte, i veure les revistes, dominis i comptes a què esteu
subscrit(e)s.
oauth2.grant.admin.entry.purge: Suprimir completament qualsevol fil de la vostra
instància.
oauth2.grant.vote.general: Votar a favor, en contra o impulsar els fils,
publicacions o comentaris.
oauth2.grant.entry_comment.all: Crear, editar o suprimir els vostres comentaris
en fils i votar, millorar o denunciar qualsevol comentari d'un fil.
oauth2.grant.entry_comment.vote: Votar a favor, impulsar o votar en contra de
qualsevol comentari d'un fil.
oauth2.grant.user.profile.edit: Editar el vostre perfil.
oauth2.grant.magazine.all: Subscriure-vos o bloquejar les revistes i veure les
revistes a què us subscriviu o heu blocat.
oauth2.grant.user.profile.read: Veure el vostre perfil.
oauth2.grant.post.report: Denunciar qualsevol publicació.
oauth2.grant.post_comment.create: Crear comentaris nous a les publicacions.
oauth2.grant.post.create: Crear publicacions noves.
oauth2.grant.moderate.entry.change_language: Canviar l'idioma dels fils a les
revistes que modereu.
oauth2.grant.user.notification.all: Llegir i eliminar les vostres notificacions.
oauth2.grant.user.oauth_clients.all: Veure i editar els permisos que heu
concedit a altres aplicacions OAuth2.
oauth2.grant.user.follow: Seguir o deixar de seguir comptes i veure una llista
de comptes que seguiu.
oauth2.grant.moderate.entry.all: Moderar els fils a les revistes que modereu.
oauth2.grant.post_comment.vote: Votar a favor, impulsar o votar en contra de
qualsevol comentari d'una publicació.
oauth2.grant.user.notification.delete: Esborrar les vostres notificacions.
oauth2.grant.user.all: Veure i editar el vostre perfil, missatges o
notificacions; veure i editar els permisos que heu concedit a altres
aplicacions; seguir o blocar altres comptes; veure les llistes de comptes que
seguiu o bloqueu.
oauth2.grant.admin.user.ban: Bandejar o desbandejar comptes de la vostra
instància.
oauth2.grant.moderate.post_comment.change_language: Canviar l'idioma dels
comentaris a les publicacions de les revistes que modereu.
oauth2.grant.admin.user.all: Bandejar, verificar o suprimir completament comptes
de la vostra instància.
oauth2.grant.moderate.post.all: Moderar les publicacions a les revistes que
modereu.
oauth2.grant.moderate.post.set_adult: Marcar com a explícites les publicacions a
les revistes que modereu.
oauth2.grant.moderate.magazine.ban.all: Gestionar els comptes bandejats a les
revistes que modereu.
oauth2.grant.admin.post.purge: Suprimir completament qualsevol publicació de la
vostra instància.
oauth2.grant.admin.instance.settings.all: Veure o actualitzar la configuració de
la vostra instància.
oauth2.grant.admin.instance.information.edit: Actualitzar les pàgines Quant a,
Preguntes freqüents, Contacte, Condicions del servei i Política de privadesa a
la vostra instància.
oauth2.grant.admin.magazine.all: Moure fils entre les revistes o suprimir-les
completament a la vostra instància.
2fa.backup: Els vostres codis de recolzament de dos factors
moderation.report.ban_user_description: Voleu bandejar el compte (%username%)
que ha creat aquest contingut d'aquesta revista?
moderation.report.approve_report_confirmation: Confirmeu l'aprovació d'aquest
informe?
purge_content_desc: Purgar completament el contingut de l'usuari(a), incloent la
supressió de les respostes d'altres usuaris(es) en fils, publicacions i
comentaris creats.
2fa.backup-create.help: Podeu crear nous codis d'autenticació de recolzament;
fer-ho invalidarà els codis existents.
subscription_header: Revistes subscrites
comment_reply_position_help: Mostrar el formulari de resposta de comentaris a la
part superior o inferior de la pàgina. Quan el «desplaçament infinit» està
habilitat, la posició sempre apareixerà a la part superior.
delete_account_desc: Suprimir el compte, incloses les respostes d'altres
usuaris(es) en fils, publicacions i comentaris creats.
oauth2.grant.moderate.post.pin: Fixar les publicacions a la part superior de les
revistes que modereu.
magazine_theme_appearance_custom_css: CSS personalitzat que s'aplicarà quan
visualitzeu contingut a la vostra revista.
two_factor_authentication: Autenticació de dos factors
2fa.disable: Desactivar l'autenticació de dos factors
2fa.setup_error: Error en activar A2F per al compte
flash_post_new_error: No s'ha pogut crear la publicació. Alguna cosa ha fallat.
account_suspended: El compte s'ha suspès.
flash_account_settings_changed: La configuració del vostre compte s'ha canviat
correctament. Haureu de tornar a iniciar sessió.
2fa.backup_codes.help: Podeu utilitzar aquests codis quan no teniu el vostre
dispositiu o aplicació d'autenticació de dos factors. No se us
tornaran a mostrar i els podreu fer servir només una
vegada .
2fa.available_apps: Utilitzar una aplicació d'autenticació de dos factors com
ara %google_authenticator%, %aegis% (Android) o %raivo% (iOS) per escanejar el
codi QR.
account_unsuspended: El compte s'ha reactivat.
deletion: Eliminació
subscription_sidebar_pop_in: Moure subscripcions al panell emergent
flash_user_edit_profile_success: La configuració del perfil s'ha desat
correctament.
flash_comment_new_success: El comentari s'ha creat correctament.
suspend_account: Suspendre el compte
flash_user_edit_email_error: No s'ha pogut canviar l'adreça electrònica.
flash_comment_new_error: No s'ha pogut crear el comentari. Alguna cosa ha
fallat.
flash_user_settings_general_success: La configuració d'usuari(a) s'ha desat
correctament.
account_is_suspended: El compte està suspès.
open_url_to_fediverse: Obrir URL original
remove_following: Eliminar el seguiment
unsuspend_account: Reactivar el compte
account_unbanned: S'ha desbandejat el compte.
deleted_by_moderator: El fil, la publicació o el comentari ha estat suprimit per
l'equip de moderació
cake_day: Des del dia
reported: denunciat(da)
notification_title_mention: Us han esmentat
someone: Algú
sso_only_mode: Restringir l'inici de sessió i el registre només als mètodes SSO
reporting_user: Denunciant
back: Tornar
admin_users_inactive: Inactius(ves)
restrict_magazine_creation: Restringir la creació de revistes locals a
l'administració i moderació global
report_subject: Assumpte
sso_show_first: Mostrar primer SSO a les pàgines d'inici de sessió i de registre
magazine_posting_restricted_to_mods: Restringir la creació de fils a la
moderació
sso_registrations_enabled.error: Els registres de comptes nous amb gestors
d'identitats de tercers estan actualment desactivats.
reported_user: Compte denunciat
related_entry: Relacionat
open_report: Obrir denúncia
own_report_rejected: La vostra denúncia ha estat rebutjada
own_content_reported_accepted: S'ha acceptat una denúncia del vostre contingut.
bookmark_list_edit: Editar
bookmark_list_selected_list: Llista seleccionada
table_of_contents: Taula de continguts
search_type_all: Tot
search_type_entry: Fils
search_type_post: Microblogs
select_user: Trieu un(a) usuari(a)
new_users_need_approval: Els comptes nous han de ser aprovats per
l'administració abans que puguin iniciar sessió.
application_text: Expliqueu per què voleu unir-vos
signup_requests: Sol·licituds de registre
signup_requests_header: Sol·licituds de registre
email_application_approved_title: La vostra sol·licitud de registre s'ha aprovat
email_application_approved_body: L'administració del servidor ha aprovat la
vostra sol·licitud de registre. Ara podeu iniciar sessió al servidor a %siteName% .
email_application_rejected_title: La vostra sol·licitud de registre ha estat
rebutjada
email_application_pending: El vostre compte requereix l'aprovació de
l'administració abans de poder iniciar sessió.
email_verification_pending: Heu de verificar la vostra adreça electrònica abans
de poder iniciar sessió.
signup_requests_paragraph: A aquests(es) usuaris(es) els agradaria unir-se al
vostre servidor. No poden iniciar sessió fins que no hàgiu aprovat llurs
sol·licituds de registre.
show_magazine_domains: Mostrar els dominis de les revistes
show_user_domains: Mostrar els dominis dels comptes
answered: respost
by: per
front_default_sort: Ordenació predeterminada de la portada
comment_default_sort: Ordenació predeterminada dels comentaris
open_signup_request: Obrir la sol·licitud de registre
image_lightbox_in_list: Les miniatures dels fils obren pantalla completa
show_users_avatars_help: Mostrar la imatge de l'avatar de l'usuari(a).
compact_view_help: Una vista compacta amb marges menors, on la miniatura passa
al costat dret.
show_magazines_icons_help: Mostrar la icona de la revista.
show_thumbnails_help: Mostrar les miniatures de les imatges.
show_new_icons: Mostrar noves icones
show_new_icons_help: Mostrar la icona per a la revista o el compte nou (30 dies
d'antiguitat o més recent)
flash_application_info: L'administració ha d'aprovar el vostre compte abans de
poder iniciar sessió. Rebreu un correu electrònic un cop s'hagi processat la
vostra sol·licitud de registre.
email_application_rejected_body: Gràcies pel vostre interès, però lamentem
informar-vos que la vostra sol·licitud de registre ha estat rebutjada.
image_lightbox_in_list_help: Quan està marcat, en fer clic a la miniatura es
mostra una finestra modal amb la imatge. Quan no estigui marcat, fer clic a la
miniatura obrirà el fil.
2fa.manual_code_hint: Si no podeu escanejar el codi QR, introduïu el secret
manualment
toolbar.emoji: Emoji
magazine_instance_defederated_info: La instància d'aquesta revista està
desfederada. Per tant, la revista no rebrà actualitzacions.
user_instance_defederated_info: La instància d'aquest compte està defederada.
flash_thread_instance_banned: La instància d'aquesta revista està banida.
show_rich_mention: Mencions enriquides
show_rich_mention_help: Mostrar un component de compte quan es menciona un
compte. Això n'inclourà el nom de visualització i la foto de perfil.
show_rich_mention_magazine: Mencions enriquides de revistes
show_rich_mention_magazine_help: Mostrar un component de revista quan es
menciona una revista. Això in'nclourà el nom de visualització i la icona.
show_rich_ap_link: Enllaços AP enriquits
show_rich_ap_link_help: Mostrar un component en línia quan s'hi enllaça un altre
contingut d'ActivityPub.
attitude: Actitud
type_search_term_url_handle: Escriviu el terme de cerca, l'URL o l'identificador
search_type_magazine: Revistes
search_type_user: Comptes
search_type_actors: Revistes i comptes
search_type_content: Temes i microblogs
type_search_magazine: Limitar la cerca a la revista...
type_search_user: Limitar la cerca a l'autoria...
modlog_type_entry_deleted: Fil suprimit
modlog_type_entry_restored: Fil restaurat
modlog_type_entry_comment_deleted: Comentari del fil suprimit
modlog_type_entry_comment_restored: Comentari del fil restaurat
modlog_type_entry_pinned: Fil fixat
modlog_type_entry_unpinned: Fil deixat de fixar
modlog_type_post_deleted: Microblog suprimit
modlog_type_post_restored: Microblog restaurat
modlog_type_post_comment_deleted: Resposta del microblog suprimida
modlog_type_post_comment_restored: Resposta del microblog restaurada
modlog_type_ban: Compte expulsat de la revista
modlog_type_moderator_add: Moderador(a) de la revista afegit(da)
modlog_type_moderator_remove: Moderador(a) de la revista destituït(da)
everyone: Tothom
nobody: Ningú
followers_only: Només seguidor(e)s
direct_message_setting_label: Qui pot enviar-vos un missatge directe
banner: Bàner
magazine_theme_appearance_banner: Bàner personalitzat per a la revista. Es
mostra a sobre de tots els fils de discussió i ha de tenir una relació
d'aspecte ampla (5:1 o 1500 px * 300 px).
delete_magazine_icon: Suprimeix icona de la revista
flash_magazine_theme_icon_detached_success: La icona de la revista s'ha suprimit
correctament
delete_magazine_banner: Suprimeix el bàner de la revista
flash_magazine_theme_banner_detached_success: El bàner de la revista s'ha
suprimit correctament
federation_page_use_allowlist_help: Si s'utilitza una llista de permesos,
aquesta instància només es federarà amb les instàncies explícitament permeses.
En cas contrari, aquesta instància es federarà amb totes les instàncies,
excepte les que estiguin prohibides.
crosspost: Publicació creuada
flash_thread_ref_image_not_found: No s'ha pogut trobar la imatge a què fa
referència 'imageHash'.
federation_uses_allowlist: Utilitzar la llista de permesos per a la federació
defederating_instance: S'està defederant la instància %i
their_user_follows: Quantitat de comptes de la seva instància que segueixen
comptes de la nostra
our_user_follows: Quantitat de comptes de la nostra instància que segueixen
comptes de la seva
their_magazine_subscriptions: Quantitat de comptes de la seva instància
subscrits a revistes de la nostra
our_magazine_subscriptions: Quantitat de comptes de la nostra instància
subscrits a revistes des de la seva
confirm_defederation: Confirmar la desfederació
flash_error_defederation_must_confirm: Heu de confirmar la desfederació
allowed_instances: Instàncies permeses
btn_deny: Denegar
btn_allow: Permetre
ban_instance: Banir instància
allow_instance: Permetre instància
front_default_content: Vista per defecte de portada
default_content_default: Valor per defecte del servidor (Fils)
default_content_threads: Fils
default_content_microblog: Microblog
combined: Combinat
sidebar_sections_random_local_only: Restringir les seccions de la barra lateral
«Publicacions/Fils aleatoris» només a locals
sidebar_sections_users_local_only: Restringir la secció de la barra lateral
«Persones actives» només a locals
random_local_only_performance_warning: Habilitar «Només aleatoris locals» pot
afectar el rendiment de l'SQL.
default_content_combined: Fils + Microblog
ban_expires: La prohibició caduca
you_have_been_banned_from_magazine: Us han prohibit l'accés a la revista %m.
you_have_been_banned_from_magazine_permanently: Us han prohibit permanentment
l'accés a la revista %m.
you_are_no_longer_banned_from_magazine: Ja no teniu prohibit l'accés a la
revista %m.
oauth2.grant.moderate.entry.lock: Bloca els fils de les revistes moderades
perquè ningú no hi pugui fer comentaris
oauth2.grant.moderate.post.lock: Bloca els microblogs a les revistes moderades,
perquè ningú no hi pugui fer comentaris
discoverable: Descobrible
user_discoverable_help: Si aquesta opció està habilitada, el vostre perfil, fils
de discussió, microblogs i comentaris es poden trobar mitjançant la cerca i
els panells aleatoris. El vostre perfil també pot aparèixer al panell
d'usuari(a) actiu(va) i a la pàgina de persones. Si aquesta opció està
desactivada, les vostres publicacions continuaran sent visibles per a altres
usuari(e)s, però no apareixeran al canal complet.
magazine_discoverable_help: Si això està habilitat, aquesta revista i els fils,
microblogs i comentaris d'aquesta revista es poden trobar mitjançant la cerca
i els panells aleatoris. Si això està desactivat, la revista encara apareixerà
a la llista de revistes, però els fils i microblogs no apareixeran al canal
complet.
flash_thread_lock_success: Fil blocat correctament
flash_thread_unlock_success: Fil desblocat correctament
flash_post_lock_success: Microblog blocat correctament
flash_post_unlock_success: Microblog desblocat correctament
lock: Blocar
unlock: Desblocar
comments_locked: Els comentaris estan blocats.
magazine_log_entry_locked: ha blocat els comentaris de
magazine_log_entry_unlocked: ha desblocat els comentaris de
modlog_type_entry_lock: Fil blocat
modlog_type_entry_unlock: Fil desblocat
modlog_type_post_lock: Microblog blocat
modlog_type_post_unlock: Microblog desblocat
contentnotification.muted: Silenciós | no rebre notificacions
contentnotification.default: Predeterminat | rebre notificacions segons la
configuració predeterminada
contentnotification.loud: Sorollós | rebre totes les notificacions
indexable_by_search_engines: Indexable pels motors de cerca
user_indexable_by_search_engines_help: Si aquesta configuració es desactiva, es
recomana als motors de cerca que no indexin cap dels vostres fils i
microblogs, però els vostres comentaris no es veuen afectats per això i els
malfactors podrien ignorar-la. Aquesta configuració també està federada a
altres servidors.
magazine_indexable_by_search_engines_help: Si aquesta configuració es desactiva,
es recomana als motors de cerca que no indexin cap dels fils i microblogs
d'aquestes revistes. Això inclou la pàgina de destinació i totes les pàgines
de comentaris. Aquesta configuració també està federada a altres servidors.
magazine_name_as_tag: Utilitza el nom de la revista com a etiqueta
magazine_name_as_tag_help: Les etiquetes d'una revista s'utilitzen per fer
coincidir les entrades de microblog amb aquesta revista. Per exemple, si el
nom és "fediverse" i les etiquetes de la revista contenen "fediverse", totes
les entrades de microblog que continguin "#fediverse" es posaran en aquesta
revista.
magazine_rules_deprecated: el camp de regles està obsolet i s'eliminarà en el
futur. Si us plau, poseu les vostres regles al quadre de descripció.
created_since: Creat des de
================================================
FILE: translations/messages.ca@valencia.yaml
================================================
type.link: Enllaç
type.article: Fil
type.video: Vídeo
type.smart_contract: Contracte intel·ligent
type.magazine: Revista
thread: Fil
threads: Fils
microblog: Microblog
people: Gent
events: Esdeveniments
magazine: Revista
search: Buscar
add: Afegir
select_channel: Trieu un canal
login: Iniciar sessió
sort_by: Ordenar per
top: Destacat
hot: Popular
active: Actiu(va)
newest: Més nou
oldest: Més vell
commented: Comentat
magazines: Revistes
type.photo: Foto
change_view: Canviar vista
filter_by_time: Filtrar per temps
filter_by_subscription: Filtrar per subscripció
filter_by_federation: Filtrar per estat de federació
comments_count: '{0}Comentaris|{1}Comentari|]1,Inf[ Comentaris'
subscribers_count: '{0}Subscriptores|{1}Subscriptora|]1,Inf[ Subscriptores'
marked_for_deletion: Marcat per a supressió
marked_for_deletion_at: Marcat per suprimir-se el %date%
favourites: Vots a favor
favourite: Preferit
more: Més
avatar: Avatar
added: Afegit
up_votes: Impulsos
down_votes: Vots en contra
no_comments: Sense comentaris
created_at: Creat
owner: Propietari(a)
subscribers: Subscritors(es)
online: En línia
comments: Comentaris
posts: Publicacions
replies: Respostes
moderators: Moderació
mod_log: Registre de moderació
add_comment: Afegir comentari
add_post: Afegir publicació
add_media: Afegir mitjà
remove_media: Eliminar mitjà
remove_user_avatar: Eliminar avatar
remove_user_cover: Eliminar portada
followers_count: '{0}Seguidores|{1}Seguidora|]1,Inf[ Seguidores'
filter_by_type: Filtrar per tipus
follow: Seguir
show_profile_followings: Mostrar els comptes seguits
blocked: Bloquejats
reports: Denúncies
overview: Visió general
edit_comment: Guardar canvis
go_to_filters: Anar a filtres
hide_adult: Amagar contingut explícit
featured_magazines: Revistes destacades
notify_on_new_entry_comment_reply: Respostes als meus comentaris en qualsevol
fil
notify_on_new_post_reply: Qualsevol nivell de respostes a les publicacions que
he escrit
notify_on_new_posts: Noves publicacions a qualsevol revista a què estic subscrit
videos: Vídeos
messages: Missatges
try_again: Torneu-ho a provar
homepage: Pàgina d'inici
badges: Insígnies
settings: Configuració
notifications: Notificacions
notify_on_new_post_comment_reply: Respostes als meus comentaris a qualsevol
publicació
appearance: Aparença
privacy: Privadesa
are_you_sure: Ho confirmeu?
repeat_password: Repetiu la contrasenya
add_new: Afegir nou
agree_terms: Accepteu els %terms_link_start%Termes i condicions%terms_link_end%
i la %policy_link_start%Política de privadesa%policy_link_end%
share: Compartir
select_magazine: Trieu una revista
reset_check_email_desc2: Si no rebeu cap correu electrònic, comproveu la vostra
carpeta de correu brossa.
tags: Etiquetes
is_adult: +18 / Explícit
domain: Domini
image_alt: Text alternatiu de la imatge
following: Seguint
subscriptions: Subscripcions
compact_view: Vista compacta
share_on_fediverse: Compartir al Fedivers
chat_view: Vista de xat
profile: Perfil
photos: Fotos
report: Denunciar
copy_url: Copiar URL de Mbin
copy_url_to_fediverse: Copiar URL original
notify_on_new_entry: Fils nous (enllaços o articles) a qualsevol revista a què
estic subscrit
edit: Editar
moderate: Moderar
reason: Motiu
edit_entry: Editar fil
show_profile_subscriptions: Mostrar subscripcions a revistes
delete: Eliminar
edit_post: Editar publicació
menu: Menú
general: General
notify_on_new_entry_reply: Qualsevol nivell de comentaris als fils que he escrit
markdown_howto: Com funciona l'editor?
enter_your_comment: Escriviu el comentari
enter_your_post: Escriviu la publicació
activity: Activitat
cover: Portada
related_posts: Publicacions relacionades
random_posts: Publicacions aleatòries
federated_magazine_info: Esta revista és d'un servidor federat i pot estar
incompleta.
disconnected_magazine_info: 'Esta revista no està rebent actualitzacions: última activitat
fa %days% dia(es).'
always_disconnected_magazine_info: Esta revista no està rebent actualitzacions.
subscribe_for_updates: Subscriviu-vos per començar a rebre actualitzacions.
federated_user_info: Este perfil és d'un servidor federat i pot ser incomplet.
go_to_original_instance: Vore en instància remota
empty: Buit
subscribe: Subscriure's
unsubscribe: Cancel·lar subscripció
unfollow: Deixar de seguir
reply: Resposta
login_or_email: Identificador o adreça electrònica
password: Contrasenya
remember_me: Recordar-me
dont_have_account: No teniu un compte?
you_cant_login: Heu oblidat la contrasenya?
already_have_account: Ja teniu un compte?
register: Crear compte
reset_password: Restablir contrasenya
show_more: Mostrar més
to: a
in: en
from: des de
username: Identificador
email: Adreça electrònica
terms: Condicions del servici
privacy_policy: Política de privadesa
about_instance: Quant a
all_magazines: Totes les revistes
stats: Estadístiques
fediverse: Fedivers
create_new_magazine: Crear revista nova
add_new_article: Afegir fil nou
add_new_link: Afegir enllaç nou
add_new_photo: Afegir foto nova
add_new_post: Afegir publicació nova
add_new_video: Afegir vídeo nou
contact: Contacte
faq: Preguntes més freqüents (PMF)
rss: RSS
change_theme: Canviar tema
downvotes_mode: Mode de vots negatius
change_downvotes_mode: Canviar el mode de vots negatius
disabled: Deshabilitat
hidden: Amagat
enabled: Habilitat
useful: Útil
help: Ajuda
check_email: Comproveu la vostra bústia electrònica
reset_check_email_desc: Si ja hi ha un compte associat a la vostra adreça
electrònica, rebreu un correu electrònic en breu amb un enllaç que podeu
utilitzar per restablir la vostra contrasenya. Este enllaç caducarà en
%expire%.
up_vote: Impulsar
down_vote: Votar en contra
email_confirm_header: Hola! Confirmeu la vostra adreça electrònica.
email_confirm_content: "Per activar el compte de Mbin feu clic a l'enllaç següent:"
email_verify: Confirmeu l'adreça electrònica
email_confirm_expire: Tingueu en compte que l'enllaç caducarà en una hora.
email_confirm_title: Confirmeu la vostra adreça electrònica.
url: URL
title: Títol
body: Cos
tag: Etiqueta
eng: ENG
oc: Cont. Orig.
image: Imatge
name: Nom
description: Descripció
rules: Normes
followers: Seguidor(e)s
cards: Targetes
columns: Columnes
user: Usuari(a)
joined: Inscrit(a)
moderated: Moderat(da)
people_local: Local
people_federated: Federat
reputation_points: Punts de reputació
related_tags: Etiquetes relacionades
go_to_content: Anar al contingut
go_to_search: Anar a la cerca
subscribed: Subscrit(a)
all: Tot
logout: Tancar sessió
classic_view: Vista clàssica
tree_view: Vista d'arbre
table_view: Vista de taula
cards_view: Vista de targetes
3h: 3h
6h: 6h
12h: 12h
1w: 1 setm.
1m: 1 mes
1d: 1 dia
1y: 1 any
links: Enllaços
articles: Fils
notify_on_user_signup: Nous registres
save: Guardar
about: Quant a
restored_post_by: ha restaurat una publicació de
size: Grandària
flash_magazine_new_success: La revista s'ha creat correctament. Ara podeu afegir
contingut nou o explorar el tauler d'administració de la revista.
edited_post: Ha editat una publicació
show_magazines_icons: Mostrar icones de les revistes
flash_thread_pin_success: El fil s'ha fixat correctament.
mod_remove_your_post: La moderació ha eliminat la vostra publicació
old_email: Adreça electrònica actual
new_email: Nova adreça electrònica
new_email_repeat: Confirmar l'adreça electrònica nova
current_password: Contrasenya actual
new_password: Contrasenya nova
new_password_repeat: Confirmar la nova contrasenya
change_email: Canviar l'adreça electrònica
change_password: Canviar la contrasenya
expand: Desplegar
collapse: Plegar
domains: Dominis
error: Error
votes: Vots
theme: Tema
dark: Fosc
light: Clar
solarized_light: Clar solaritzat
solarized_dark: Fosc solaritzat
default_theme: Tema predeterminat
default_theme_auto: Clar/fosc (detecció automàtica)
solarized_auto: Solaritzat (detecció automàtica)
font_size: Grandària de la lletra
boosts: Impulsos
show_users_avatars: Mostrar avatars d'usuaris(es)
yes: Sí
no: No
show_thumbnails: Mostrar miniatures
rounded_edges: Vores arredonides
removed_thread_by: ha eliminat un fil de
restored_thread_by: ha restaurat un fil de
removed_comment_by: ha eliminat un comentari de
restored_comment_by: ha restaurat el comentari de
removed_post_by: ha eliminat una publicació de
he_banned: vetat(da)
he_unbanned: vet retirat
read_all: Marcar-ho tot com a llegit
show_all: Mostrar-ho tot
flash_register_success: Benvinguda a bord! El vostre compte ja està registrat.
Un últim pas - consulteu la vostra safata d'entrada per a rebre un enllaç
d'activació que donarà vida al vostre compte.
flash_thread_new_success: El fil s'ha creat correctament i ara és visible per a
altres usuaris(es).
flash_thread_edit_success: El fil s'ha editat correctament.
flash_thread_delete_success: El fil s'ha suprimit correctament.
flash_thread_unpin_success: El fil s'ha desfixat correctament.
flash_magazine_edit_success: La revista s'ha editat correctament.
flash_mark_as_adult_success: La publicació s'ha marcat correctament com a
explícita.
flash_unmark_as_adult_success: La publicació s'ha desmarcat correctament com a
explícita.
too_many_requests: S'ha superat el límit; torneu-ho a provar més tard.
set_magazines_bar: Barra de revistes
set_magazines_bar_desc: afegiu els noms de les revistes després de la coma
set_magazines_bar_empty_desc: si el camp està buit, les revistes actives es
mostren a la barra.
mod_log_alert: 'ADVERTÈNCIA: En el registre de moderació podreu trobar contingut desagradable
o ofensiu eliminat per la moderació. Assegureu-vos de saber el que esteu fent.'
added_new_thread: S'ha afegit un fil nou
edited_thread: Ha editat un fil
mod_remove_your_thread: La moderació ha eliminat el vostre fil
added_new_comment: Ha afegit un comentari nou
edited_comment: Ha editat un comentari
replied_to_your_comment: Ha respost al vostre comentari
mod_deleted_your_comment: La moderació ha suprimit el vostre comentari
added_new_post: Ha afegit una publicació nova
added_new_reply: Ha afegit una nova resposta
post: Publicació
comment: Comentari
mentioned_you: Vos ha esmentat
ban_expired: El vet ha expirat
wrote_message: Ha escrit un missatge
banned: Vos ha vetat
removed: Eliminat per la moderació
deleted: Esborrat per l'autor
message: Missatge
infinite_scroll: Desplaçament infinit
show_top_bar: Mostrar barra superior
subject_reported: S'ha denunciat el contingut.
left: Esquerra
right: Dreta
federation: Federació
status: Estat
upload_file: Pujar arxiu
approve: Aprovar
approved: Aprovat
rejected: Rebutjat
add_moderator: Afegir moderador(a)
add_badge: Afegir insígnia
created: Creat
expires: Caduca
perm: Permanent
trash: Paperera
icon: Icona
done: Fet
pin: Fixar
unpin: Desfixar
unban: Retirar vet
unban_hashtag_btn: Retirar vet al hashtag
unban_hashtag_description: Retirar el vet a un hashtag permetrà tornar a crear
publicacions amb este hashtag. Les publicacions existents amb el hashtag ja no
s'amaguen.
add_ban: Afegir vet
ban: Vetar
ban_hashtag_btn: Vetar hashtag
bans: Vets
change_magazine: Canviar revista
change_language: Canviar idioma
mark_as_adult: Marcar com a explícit
change: Canviar
writing: Escriptura
users: Usuaris(es)
restore: Restaurar
add_mentions_entries: Afegir etiquetes de menció als fils
add_mentions_posts: Afegir etiquetes de menció a les publicacions
Password is invalid: La contrasenya no és vàlida.
Your account is not active: El vostre compte no està actiu.
firstname: Nom
send: Enviar
active_users: Persones actives
random_entries: Fils aleatoris
related_entries: Fils relacionats
delete_account: Suprimir el compte
Your account has been banned: El vostre compte ha sigut vetat.
related_magazines: Revistes relacionades
random_magazines: Revistes aleatòries
unban_account: Retirar vet al compte
ban_account: Vetar el compte
banned_instances: Instàncies prohibides
kbin_intro_title: Explorar el fedivers
kbin_promo_title: Creeu la vostra pròpia instància
captcha_enabled: Captcha activat
return: Tornar
boost: Impulsar
mercure_enabled: Mercure activat
tokyo_night: Nit de Tòquio
preferred_languages: Filtrar els idiomes de fils i publicacions
infinite_scroll_help: Carregar automàticament més contingut en arribar a la part
inferior de la pàgina.
sticky_navbar_help: La barra de navegació es fixarà a la part superior de la
pàgina quan vos desplaceu cap avall.
auto_preview_help: Mostrar les previsualitzacions multimèdia (foto, vídeo) en
una grandària major baix del contingut.
reload_to_apply: Torneu a carregar la pàgina per aplicar els canvis
filter.fields.label: Trieu quins camps voleu buscar
filter.adult.label: Trieu si voleu mostrar contingut explícit
filter.adult.hide: Amagar contingut explícit
filter.adult.only: Només el contingut explícit
local_and_federated: Local i federat
kbin_bot: Agent Mbin
your_account_has_been_banned: El vostre compte ha sigut vetat
toolbar.bold: Negreta
toolbar.image: Imatge
toolbar.unordered_list: Llista no ordenada
toolbar.ordered_list: Llista ordenada
toolbar.mention: Esment
toolbar.spoiler: Spoiler
federation_page_enabled: Pàgina de federació activada
federation_page_allowed_description: Instàncies conegudes amb què ens federem
federated_search_only_loggedin: Cerca federada limitada si no s'ha iniciat
sessió
account_deletion_title: Supressió del compte
account_deletion_button: Suprimir el compte
account_deletion_immediate: Suprimir immediatament
more_from_domain: Més del domini
errors.server500.title: 500 Error intern del servidor
errors.server500.description: Ho sentim, hi ha hagut un error al nostre costat.
Si continueu veient l'error, proveu de contactar amb l'administració de la
instància. Si la instància no funciona en absolut, aneu a %link_start%altres
instàncies de Mbin%link_end% mentrestant fins que es resolga el problema.
errors.server429.title: 429 Massa sol·licituds
errors.server403.title: 403 Prohibit
email_confirm_button_text: Confirmeu la vostra sol·licitud de canvi de
contrasenya
email_confirm_link_help: Alternativament, podeu copiar i pegar el següent al
vostre navegador
email.delete.title: Sol·licitud d'eliminació del compte
email.delete.description: L'usuari(a) següent ha sol·licitat que s'elimine el
seu compte
resend_account_activation_email_question: Compte inactiu?
resend_account_activation_email: Tornar a enviar el correu electrònic
d'activació del compte
resend_account_activation_email_success: Si existix un compte associat amb
l'adreça electrònica, hi enviarem un nou correu d'activació.
resend_account_activation_email_description: Introduïu l'adreça electrònica
associada al vostre compte. Vos hi enviarem un altre correu d'activació.
oauth.consent.title: Formulari de consentiment OAuth2
oauth.consent.grant_permissions: Concedir permisos
oauth.consent.app_requesting_permissions: voldria realitzar les accions següents
en nom vostre
oauth.consent.to_allow_access: Per permetre este accés feu clic al botó
«Permetre” a continuació
oauth.consent.allow: Permetre
oauth.consent.deny: Denegar
oauth.client_identifier.invalid: Identificador de client OAuth no vàlid!
oauth.client_not_granted_message_read_permission: Esta aplicació no ha rebut
permís per llegir els vostres missatges.
restrict_oauth_clients: Restringir la creació de clients OAuth2 a
l'administració
block: Bloquejar
unblock: Desbloquejar
oauth2.grant.moderate.magazine.reports.action: Acceptar o rebutjar denúncies a
les revistes que modereu.
oauth2.grant.moderate.magazine.trash.read: Veure el contingut a la paperera de
les revistes que modereu.
oauth2.grant.moderate.magazine_admin.all: Crear, editar o suprimir les vostres
revistes.
oauth2.grant.moderate.magazine_admin.create: Crear noves revistes.
oauth2.grant.moderate.magazine_admin.delete: Suprimir qualsevol de les vostres
revistes.
oauth2.grant.moderate.magazine_admin.update: Editar les regles, la descripció,
el mode explícit o la icona de les vostres revistes.
oauth2.grant.moderate.magazine_admin.moderators: Afegir o eliminar moderador(e)s
de qualsevol de les vostres revistes.
oauth2.grant.moderate.magazine_admin.badges: Crear o eliminar insígnies de les
vostres revistes.
oauth2.grant.moderate.magazine_admin.tags: Crear o eliminar etiquetes de les
vostres revistes.
oauth2.grant.admin.entry.purge: Suprimir completament qualsevol fil de la vostra
instància.
oauth2.grant.report.general: Denunciar fils, publicacions o comentaris.
oauth2.grant.vote.general: Votar a favor, en contra o impulsar els fils,
publicacions o comentaris.
oauth2.grant.subscribe.general: Subscriure-vos o seguir qualsevol revista,
domini o compte, i veure les revistes, dominis i comptes a què esteu
subscrit(e)s.
oauth2.grant.block.general: Bloquejar o desbloquejar qualsevol revista, domini o
compte i veure les revistes, dominis i comptes que heu bloquejat.
oauth2.grant.domain.all: Subscriure-vos o bloquejar dominis i veure els dominis
a què us subscriviu o que bloquegeu.
oauth2.grant.magazine.all: Subscriure-vos o bloquejar les revistes i veure les
revistes a què vos subscriviu o heu bloquejat.
oauth2.grant.magazine.subscribe: Subscriure-vos o cancel·lar la subscripció a
revistes i veure les revistes a què vos subscriviu.
oauth2.grant.magazine.block: Bloquejar o desbloquejar revistes i veure les
revistes que heu bloquejat.
oauth2.grant.post.all: Crear, editar o suprimir els vostres microblogs i votar,
impulsar o denunciar qualsevol microblog.
oauth2.grant.post.create: Crear publicacions noves.
oauth2.grant.post.edit: Editar les vostres publicacions existents.
oauth2.grant.post.delete: Suprimir les vostres publicacions existents.
oauth2.grant.post.vote: Votar a favor, impulsar o votar en contra de qualsevol
publicació.
oauth2.grant.post.report: Denunciar qualsevol publicació.
oauth2.grant.user.bookmark: Afegir i eliminar marcadors
oauth2.grant.user.bookmark.add: Afegir marcadors
oauth2.grant.user.bookmark.remove: Eliminar marcadors
oauth2.grant.user.bookmark_list: Veure, editar i suprimir les vostres llistes de
marcadors
oauth2.grant.user.bookmark_list.read: Veure les vostres llistes de marcadors
oauth2.grant.user.bookmark_list.edit: Editar les vostres llistes de marcadors
oauth2.grant.user.bookmark_list.delete: Esborrar les vostres llistes de
marcadors
oauth2.grant.user.profile.all: Llegir i editar el vostre perfil.
oauth2.grant.user.oauth_clients.edit: Editar els permisos que heu concedit a
altres aplicacions OAuth2.
oauth2.grant.user.follow: Seguir o deixar de seguir comptes i veure una llista
de comptes que seguiu.
oauth2.grant.moderate.entry_comment.set_adult: Marcar els comentaris als fils
com a explícits a les revistes que modereu.
oauth2.grant.moderate.entry_comment.trash: Ficar a la paperera o restaurar els
comentaris dels fils de les revistes que modereu.
oauth2.grant.moderate.post.set_adult: Marcar com a explícites les publicacions a
les revistes que modereu.
oauth2.grant.moderate.magazine.all: Gestionar els vets, les denúncies i veure
els articles a la paperera a les revistes que modereu.
oauth2.grant.moderate.magazine.ban.read: Veure els comptes vetats a les revistes
que modereu.
oauth2.grant.admin.user.ban: Vetar o retirar vet a comptes de la vostra
instància.
oauth2.grant.moderate.magazine.ban.create: Vetar comptes a les revistes que
modereu.
oauth2.grant.admin.entry_comment.purge: Suprimir completament qualsevol
comentari d'un fil de la vostra instància.
oauth2.grant.admin.magazine.purge: Suprimir completament les revistes de la
vostra instància.
oauth2.grant.admin.user.delete: Eliminar comptes de la vostra instància.
oauth2.grant.admin.user.purge: Eliminar completament comptes de la vostra
instància.
oauth2.grant.admin.instance.all: Veure i actualitzar la configuració o la
informació de la instància.
oauth2.grant.admin.user.verify: Verificar usuaris(es) a la vostra instància.
oauth2.grant.admin.instance.stats: Veure les estadístiques de la vostra
instància.
oauth2.grant.admin.instance.settings.read: Veure la configuració de la vostra
instància.
oauth2.grant.admin.instance.settings.edit: Actualitzar la configuració de la
vostra instància.
flash_post_pin_success: La publicació s'ha fixat correctament.
flash_post_unpin_success: La publicació s'ha desfixat correctament.
comment_reply_position_help: Mostrar el formulari de resposta de comentaris a la
part superior o inferior de la pàgina. Quan el «desplaçament infinit» està
habilitat, la posició sempre apareixerà a la part superior.
show_avatars_on_comments: Mostrar avatars als comentaris
single_settings: Únic
update_comment: Actualitzar comentari
show_avatars_on_comments_help: Mostrar/amagar els avatars quan es veuen
comentaris en un sol fil o publicació.
comment_reply_position: Posició del comentari de resposta
magazine_theme_appearance_custom_css: CSS personalitzat que s'aplicarà quan
visualitzeu contingut a la vostra revista.
magazine_theme_appearance_icon: Icona personalitzada per a la revista.
moderation.report.ban_user_title: Vetar compte
moderation.report.approve_report_confirmation: Confirmeu l'aprovació d'aquest
informe?
subject_reported_exists: Aquest contingut ja s'ha denunciat.
moderation.report.reject_report_confirmation: Confirmeu que voleu rebutjar
aquesta denúncia?
delete_content: Suprimir contingut
purge_content: Purgar contingut
schedule_delete_account_desc: Programar la supressió d'aquest compte en 30 dies.
Açò amagarà l'usuari(a) i el seu contingut, i també impedirà que inicie
sessió.
remove_schedule_delete_account: Cancel·lar la supressió programada
two_factor_authentication: Autenticació de dos factors
two_factor_backup: Codis de recolzament d'autenticació de dos factors
2fa.verify: Verificar
2fa.code_invalid: El codi d'autenticació no és vàlid
2fa.enable: Configurar l'autenticació de dos factors
2fa.disable: Desactivar l'autenticació de dos factors
2fa.backup: Els vostres codis de recolzament de dos factors
delete_account_desc: Suprimir el compte, incloses les respostes d'altres
usuaris(es) en fils, publicacions i comentaris creats.
2fa.verify_authentication_code.label: Introduïu un codi de dos factors per
verificar la configuració
2fa.qr_code_link.title: En visitar aquest enllaç permetreu a la vostra
plataforma registrar aquesta autenticació de dos factors
2fa.user_active_tfa.title: L'usuari(a) té actiu el doble factor d'autenticació
2fa.backup_codes.help: Podeu emprar estos codis quan no teniu el vostre
dispositiu o aplicació d'autenticació de dos factors. No se vos
tornaran a mostrar i els podreu fer servir només una
vegada .
2fa.backup_codes.recommendation: Guardeu-ne una còpia en un lloc segur.
flash_account_settings_changed: La configuració del vostre compte s'ha canviat
correctament. Haureu de tornar a iniciar sessió.
subscriptions_in_own_sidebar: A una barra lateral separada
sidebars_same_side: Barres laterals al mateix costat
subscription_sidebar_pop_out_right: Moure a la barra lateral separada de la
dreta
subscription_sidebar_pop_out_left: Moure a la barra lateral separada de
l'esquerra
subscription_sidebar_pop_in: Moure subscripcions al panell emergent
subscription_panel_large: Panell gran
subscription_header: Revistes subscrites
close: Tancar
position_top: Superior
pending: Pendent
flash_thread_new_error: No s'ha pogut crear el fil. Alguna cosa ha fallat.
flash_thread_tag_banned_error: No s'ha pogut crear el fil. El contingut no està
permès.
flash_image_download_too_large_error: La imatge no s'ha pogut crear, és massa
gran (grandària màxima %bytes%)
flash_email_was_sent: El correu electrònic s'ha enviat correctament.
flash_post_new_error: No s'ha pogut crear la publicació. Alguna cosa ha fallat.
flash_magazine_theme_changed_success: S'ha actualitzat correctament l'aparença
de la revista.
flash_magazine_theme_changed_error: No s'ha pogut actualitzar l'aparença de la
revista.
flash_comment_new_success: El comentari s'ha creat correctament.
flash_comment_edit_success: El comentari s'ha actualitzat correctament.
flash_comment_new_error: No s'ha pogut crear el comentari. Alguna cosa ha
fallat.
flash_user_settings_general_error: No s'ha pogut guardar la configuració
d'usuari(a).
flash_user_edit_profile_error: No s'ha pogut guardar la configuració del perfil.
flash_user_edit_profile_success: La configuració del perfil s'ha guardat
correctament.
flash_user_edit_email_error: No s'ha pogut canviar l'adreça electrònica.
flash_user_edit_password_error: No s'ha pogut canviar la contrasenya.
flash_thread_edit_error: No s'ha pogut editar el fil. Alguna cosa ha fallat.
flash_post_edit_success: La publicació s'ha editat correctament.
page_width: Amplària de pàgina
page_width_max: Màxim
page_width_auto: Automàtic
page_width_fixed: Fix
filter_labels: Filtrar etiquetes
auto: Automàtic
open_url_to_fediverse: Obrir URL original
change_my_avatar: Canviar el meu avatar
change_my_cover: Canviar la meua portada
edit_my_profile: Editar el meu perfil
account_settings_changed: La configuració del vostre compte s'ha canviat
correctament. Haureu de tornar a iniciar sessió.
magazine_deletion: Eliminació de la revista
delete_magazine: Suprimir revista
restore_magazine: Recuperar revista
purge_magazine: Purgar revista
magazine_is_deleted: S'ha suprimit la revista. Podeu recuperar-la en un termini de 30 dies.
suspend_account: Suspendre el compte
unsuspend_account: Reactivar el compte
account_suspended: El compte s'ha suspès.
deletion: Eliminació
account_unbanned: S'ha retirat el vet al compte.
account_is_suspended: El compte està suspès.
remove_subscriptions: Eliminar subscripcions
apply_for_moderator: Sol·licitar ser moderador(a)
cancel_request: Cancel·lar sol·licitud
action: Acció
user_badge_op: OP
user_badge_admin: Administrador(a)
user_badge_global_moderator: Moderador(a) global
user_badge_moderator: Moderador(a)
user_badge_bot: Bot
announcement: Anunci
deleted_by_author: L'autor(a) ha eliminat el fil, la publicació o el comentari
sensitive_toggle: Commutar la visibilitat del contingut sensible
sensitive_hide: Feu clic per amagar
details: Detalls
edited: editat
sso_registrations_enabled: Registres SSO activats
sso_only_mode: Restringir l'inici de sessió i el registre només als mètodes SSO
related_entry: Relacionat
sso_show_first: Mostrar primer SSO a les pàgines d'inici de sessió i de registre
reporting_user: Denunciant
reported: denunciat(da)
own_content_reported_accepted: S'ha acceptat una denúncia del vostre contingut.
open_report: Obrir denúncia
cake_day: Des del dia
someone: Algú
magazine_log_mod_added: ha afegit un(a) moderador(a)
magazine_log_entry_unpinned: s'ha eliminat l'entrada fixada
unregister_push_notifications_button: Eliminar el registre de «push»
test_push_notifications_button: Provar les notificacions «push»
notification_title_removed_comment: S'ha eliminat un comentari
notification_title_edited_comment: S'ha editat un comentari
notification_title_mention: Vos han esmentat
notification_title_new_reply: Nova resposta
notification_title_new_thread: Nou fil
notification_title_removed_thread: S'ha eliminat un fil
notification_title_ban: Vos han vetat
notification_title_edited_thread: S'ha editat un fil
notification_body2_new_signup_approval: Heu d'aprovar la sol·licitud abans que
puguen iniciar sessió
show_related_magazines: Mostrar revistes aleatòries
show_related_entries: Mostrar fils aleatoris
show_related_posts: Mostrar publicacions aleatòries
show_active_users: Mostrar comptes actius
flash_posting_restricted_error: La creació de fils està restringida a la
moderació d'aquesta revista i no en sou part
server_software: Programari del servidor
version: Versió
last_successful_deliver: Última entrega correcta
last_successful_receive: Última rebuda correcta
last_failed_contact: Últim contacte fallit
magazine_posting_restricted_to_mods: Restringir la creació de fils a la
moderació
admin_users_banned: Vetats(des)
admin_users_active: Actius(ves)
admin_users_suspended: Suspesos(es)
user_verify: Activar compte
max_image_size: Grandària màxima del fitxer
comment_not_found: No s'ha trobat el comentari
bookmark_remove_all: Eliminar tots els marcadors
count: Recompte
is_default: És predeterminada
bookmark_list_is_default: És la llista predeterminada
bookmark_list_create: Crear
bookmark_list_create_placeholder: escriviu el nom…
bookmarks_list_edit: Editar la llista de marcadors
search_type_entry: Fils
search_type_post: Microblogs
select_user: Trieu un(a) usuari(a)
signup_requests: Sol·licituds de registre
application_text: Expliqueu per què voleu unir-vos
signup_requests_header: Sol·licituds de registre
signup_requests_paragraph: A estos(es) usuaris(es) els agradaria unir-se al
vostre servidor. No poden iniciar sessió fins que no hàgeu aprovat llurs
sol·licituds de registre.
email_application_approved_title: La vostra sol·licitud de registre s'ha aprovat
email_application_approved_body: L'administració del servidor ha aprovat la
vostra sol·licitud de registre. Ara podeu iniciar sessió al servidor a %siteName% .
email_application_rejected_title: La vostra sol·licitud de registre ha sigut
rebutjada
show_magazine_domains: Mostrar els dominis de les revistes
show_user_domains: Mostrar els dominis dels comptes
image_lightbox_in_list: Les miniatures dels fils obren pantalla completa
show_magazines_icons_help: Mostrar la icona de la revista.
show_thumbnails_help: Mostrar les miniatures de les imatges.
image_lightbox_in_list_help: Quan està marcat, en fer clic a la miniatura es
mostra una finestra modal amb la imatge. Quan no estiga marcat, fer clic a la
miniatura obrirà el fil.
show_new_icons: Mostrar noves icones
show_new_icons_help: Mostrar la icona per a la revista o el compte nou (30 dies
d'antiguitat o més recent)
off: Apagat
note: Nota
header_logo: Logotip de la capçalera
unmark_as_adult: Desmarcar com a explícit
local: Local
article: Fil
month: Mes
reputation: Reputació
year: Any
registrations_enabled: Registre activat
FAQ: Preguntes més freqüents (PMF)
months: Mesos
dashboard: Tauler de control
federated: Federat
contact_email: Adreça electrònica de contacte
instance: Instància
your_account_is_not_yet_approved: El vostre compte encara no s'ha aprovat.
Enviarem un correu electrònic quan l'administració haja processat la vostra
sol·licitud de registre.
toolbar.link: Enllaç
on: Encés
purge: Buidar la llista
reject: Rebutjar
instances: Instàncies
from_url: Des de l'URL
type_search_term: Escriviu el terme de cerca
browsing_one_thread: Només esteu navegant per un fil de la discussió! Tots els
comentaris estan disponibles a la pàgina de publicació.
toolbar.strikethrough: Ratllat
send_message: Enviar missatge directe
admin_panel: Tauler d'administració
filter.origin.label: Trieu l'origen
meta: Meta
kbin_promo_desc: '%link_start%Cloneu el repositori%link_end% i desenvolupeu fedivers'
sidebar_position: Posició de la barra lateral
preview: Previsualitzar
registration_disabled: Registre desactivat
purge_account: Purgar el compte
filters: Filtres
weeks: Setmanes
pinned: Fixat
ban_hashtag_description: Vetar un hashtag impedirà que es creen publicacions amb
este hashtag i amagarà les publicacions existents que el tinguen.
week: Setmana
sticky_navbar: Barra de navegació fixa
federation_enabled: Federació activada
content: Contingut
magazine_panel_tags_info: Indiqueu-ho només si voleu que el contingut del
fedivers s'incloga en esta revista segons les etiquetes
magazine_panel: Panell de la revista
expired_at: Caducà el
password_confirm_header: Confirmeu la vostra sol·licitud de canvi de
contrasenya.
toolbar.header: Capçalera
toolbar.quote: Cita
filter.fields.names_and_descriptions: Noms i descripcions
dynamic_lists: Llistes dinàmiques
pages: Pàgines
sidebar: Barra lateral
report_issue: Denunciar problema
filter.fields.only_names: Només noms
auto_preview: Vista prèvia automàtica dels mitjans
filter.adult.show: Mostrar el contingut explícit
kbin_intro_desc: és una plataforma descentralitzada per a l'agregació de
continguts i microblogging que opera dins de la xarxa fedivers.
viewing_one_signup_request: Només esteu veient una sol·licitud de registre de
%username%
your_account_is_not_active: El vostre compte no s'ha activat. Comproveu la
vostra bústia electrònica per obtenir instruccions d'activació del compte o sol·liciteu un correu electrònic d'activació del compte
nou.
toolbar.italic: Itàlica
toolbar.code: Codi
oauth2.grant.moderate.magazine.ban.all: Gestionar els comptes vetats a les
revistes que modereu.
account_deletion_description: El vostre compte se suprimirà d'aquí a 30 dies
tret que decidiu suprimir-lo immediatament. Per restaurar el vostre compte en
un termini de 30 dies, inicieu sessió amb les mateixes credencials o poseu-vos
en contacte amb l'equip d'administració.
oauth2.grant.moderate.magazine_admin.edit_theme: Editar el CSS personalitzat de
qualsevol de les vostres revistes.
oauth2.grant.admin.all: Realitzar qualsevol acció administrativa sobre la vostra
instància.
oauth2.grant.moderate.entry.all: Moderar els fils a les revistes que modereu.
oauth2.grant.entry_comment.all: Crear, editar o suprimir els vostres comentaris
en fils i votar, millorar o denunciar qualsevol comentari d'un fil.
oauth2.grant.user.notification.read: Llegir les vostres notificacions, incloses
les notificacions de missatges.
oauth2.grant.delete.general: Suprimir qualsevol dels vostres fils, publicacions
o comentaris.
federation_page_dead_title: Instàncies mortes
oauth2.grant.entry.delete: Suprimir els vostres fils existents.
federation_page_dead_description: Instàncies en què no vam poder entregar
almenys 10 activitats seguides i on l'última entrega i recepció amb èxit van
ser fa més d'una setmana
custom_css: CSS personalitzat
oauth2.grant.domain.block: Bloquejar o desbloquejar dominis i veure els dominis
que heu bloquejat.
oauth2.grant.moderate.entry_comment.all: Moderar els comentaris als fils de les
revistes que modereu.
oauth2.grant.domain.subscribe: Subscriure-vos o cancel·lar la subscripció als
dominis i veure els dominis a què vos subscriviu.
oauth2.grant.post_comment.vote: Votar a favor, impulsar o votar en contra de
qualsevol comentari d'una publicació.
oauth2.grant.moderate.magazine_admin.stats: Veure el contingut, votar i
consultar les estadístiques de les vostres revistes.
oauth2.grant.admin.magazine.all: Moure fils entre les revistes o suprimir-les
completament a la vostra instància.
show_subscriptions: Mostrar subscripcions
oauth2.grant.entry.all: Crear, editar o suprimir els vostres fils i votar,
impulsar o denunciar qualsevol fil.
federation_page_disallowed_description: Instàncies amb què no ens federem
resend_account_activation_email_error: Hi ha hagut un problema en enviar la
sol·licitud. Potser no hi ha cap compte associat amb l'adreça electrònica o
potser ja està activat.
ignore_magazines_custom_css: Ignorar el CSS personalitzat de les revistes
errors.server404.title: 404 No trobat
private_instance: Forçar a iniciar sessió abans de poder accedir a qualsevol
contingut
oauth2.grant.user.notification.all: Llegir i eliminar les vostres notificacions.
oauth2.grant.write.general: Crear o editar qualsevol dels vostres fils,
publicacions o comentaris.
oauth2.grant.admin.user.all: Vetar, verificar o suprimir completament comptes de
la vostra instància.
alphabetically: Alfabèticament
oauth2.grant.moderate.magazine.ban.delete: Retirar vet a usuaris(es) de les
revistes que modereu.
oauth.consent.app_has_permissions: ja pot realitzar les accions següents
oauth2.grant.moderate.magazine.list: Mostrar la llista de les revistes que
modereu.
oauth2.grant.user.all: Veure i editar el vostre perfil, missatges o
notificacions; veure i editar els permisos que heu concedit a altres
aplicacions; seguir o bloquejar altres comptes; veure les llistes de comptes
que seguiu o bloquegeu.
position_bottom: Inferior
oauth2.grant.moderate.magazine.reports.all: Gestionar les denúncies a les
revistes que modereu.
flash_post_edit_error: No s'ha pogut editar la publicació. Alguna cosa ha
fallat.
continue_with: Continuar amb
oauth2.grant.moderate.magazine.reports.read: Mostrar les denúncies a les
revistes que modereu.
oauth2.grant.entry_comment.edit: Editar els vostres comentaris existents als
fils.
oauth2.grant.read.general: Llegir tot el contingut a què tingueu accés.
oauth2.grant.entry.create: Crear fils nous.
oauth2.grant.entry_comment.create: Crear comentaris nous en fils.
oauth2.grant.entry_comment.vote: Votar a favor, impulsar o votar en contra de
qualsevol comentari d'un fil.
oauth2.grant.entry.edit: Editar els vostres fils existents.
oauth2.grant.entry_comment.delete: Suprimir els vostres comentaris existents als
fils.
oauth2.grant.entry_comment.report: Denunciar qualsevol comentari en un fil.
oauth2.grant.moderate.post.change_language: Canviar l'idioma de les publicacions
a les revistes que modereu.
moderation.report.reject_report_title: Rebutjar la denúncia
and: i
oauth2.grant.entry.vote: Votar a favor, impulsar o votar en contra de qualsevol
fil.
oauth2.grant.moderate.entry.set_adult: Marcar els fils com a explícits a les
revistes que modereu.
oauth2.grant.entry.report: Denunciar qualsevol fil.
oauth2.grant.post_comment.edit: Editar els vostres comentaris existents a les
publicacions.
report_subject: Assumpte
oauth2.grant.post_comment.delete: Suprimir els vostres comentaris existents a
les publicacions.
oauth2.grant.post_comment.report: Denunciar qualsevol comentari en una
publicació.
oauth2.grant.user.message.create: Enviar missatges a altres usuaris(es).
oauth2.grant.user.message.read: Llegir els vostres missatges.
oauth2.grant.user.oauth_clients.all: Veure i editar els permisos que heu
concedit a altres aplicacions OAuth2.
oauth2.grant.moderate.entry_comment.change_language: Canviar l'idioma dels
comentaris als fils de les revistes que modereu.
oauth2.grant.moderate.post_comment.trash: Ficar a la paperera o restaurar els
comentaris a les publicacions de les revistes que modereu.
oauth2.grant.post_comment.all: Crear, editar o suprimir els vostres comentaris a
les publicacions i votar, impulsar o denunciar qualsevol comentari en una
publicació.
oauth2.grant.post_comment.create: Crear comentaris nous a les publicacions.
oauth2.grant.user.profile.read: Veure el vostre perfil.
oauth2.grant.moderate.post_comment.change_language: Canviar l'idioma dels
comentaris a les publicacions de les revistes que modereu.
oauth2.grant.user.profile.edit: Editar el vostre perfil.
oauth2.grant.user.block: Bloquejar o desbloquejar comptes i veure una llista de
comptes que bloquegeu.
oauth2.grant.moderate.post.all: Moderar les publicacions a les revistes que
modereu.
oauth2.grant.moderate.post.trash: Ficar a la paperera o restaurar les
publicacions de les revistes que modereu.
oauth2.grant.admin.post_comment.purge: Suprimir completament qualsevol comentari
d'una publicació de la vostra instància.
sensitive_show: Feu clic per mostrar
oauth2.grant.user.message.all: Llegir els vostres missatges i enviar missatges a
altres usuaris(es).
oauth2.grant.user.notification.delete: Esborrar les vostres notificacions.
oauth2.grant.admin.federation.all: Veure i actualitzar les instàncies
desfederades actualment.
2fa.backup-create.label: Crear codis d'autenticació de recolzament nous
2fa.remove: Eliminar l'autenticació de dos factors
oauth2.grant.user.oauth_clients.read: Veure els permisos que heu concedit a
altres aplicacions OAuth2.
remove_schedule_delete_account_desc: Cancel·lar la programació de l'eliminació.
Tot el contingut tornarà a estar disponible i el compte podrà iniciar sessió.
2fa.qr_code_img.alt: Un codi QR que permet configurar l'autenticació de dos
factors per al vostre compte
notification_title_new_post: Publicació nova
oauth2.grant.moderate.all: Realitzar qualsevol acció de moderació que tingueu
permís per dur a terme a les revistes que modereu.
oauth2.grant.moderate.entry.change_language: Canviar l'idioma dels fils a les
revistes que modereu.
oauth2.grant.moderate.entry.pin: Fixar els fils a la part superior de les
revistes que modereu.
request_magazine_ownership: Demanar la propietat de la revista
reported_user: Compte denunciat
last_updated: Última actualització
oauth2.grant.moderate.entry.trash: Ficar a la paperera o restaurar fils a les
revistes que modereu.
oauth2.grant.admin.federation.update: Afegir o eliminar instàncies de la llista
d'instàncies desfederades.
oauth2.grant.moderate.post_comment.all: Moderar els comentaris a les
publicacions de les revistes que modereu.
oauth2.grant.moderate.post_comment.set_adult: Marcar com a explícits els
comentaris de les publicacions a les revistes que modereu.
oauth2.grant.admin.federation.read: Veure la llista d'instàncies desfederades.
all_time: Tot el temps
email_application_rejected_body: Gràcies pel vostre interés, però lamentem
informar-vos que la vostra sol·licitud de registre ha sigut rebutjada.
moderation.report.ban_user_description: Voleu vetar el compte (%username%) que
ha creat aquest contingut d'aquesta revista?
abandoned: Abandonat
oauth2.grant.admin.magazine.move_entry: Moure fils entre revistes a la vostra
instància.
last_active: Última activitat
moderation.report.approve_report_title: Aprovar la denúncia
2fa.add: Afegir al meu compte
oauth2.grant.admin.oauth_clients.all: Veure o revocar els clients OAuth2 que
existeixen a la vostra instància.
bookmark_add_to_list: Afegir marcador a %list%
oauth2.grant.admin.post.purge: Suprimir completament qualsevol publicació de la
vostra instància.
user_suspend_desc: En suspendre el compte s'amaga el contingut a la instància,
però no l'elimina permanentment i el podeu restaurar en qualsevol moment.
magazine_theme_appearance_background_image: Imatge de fons personalitzada que
s'aplicarà quan visualitzeu contingut a la vostra revista.
account_unsuspended: El compte s'ha reactivat.
show: Mostrar
oauth2.grant.admin.instance.settings.all: Veure o actualitzar la configuració de
la vostra instància.
show_users_avatars_help: Mostrar la imatge de l'avatar de l'usuari(a).
oauth2.grant.admin.instance.information.edit: Actualitzar les pàgines Quant a,
Preguntes freqüents, Contacte, Condicions del servei i Política de privadesa a
la vostra instància.
keywords: Paraules clau
oauth2.grant.admin.oauth_clients.read: Veure els clients OAuth2 que existeixen a
la vostra instància i les seves estadístiques d'ús.
back: Tornar
bookmark_add_to_default_list: Afegir marcador a la llista predeterminada
oauth2.grant.admin.oauth_clients.revoke: Revocar l'accés als clients OAuth2 a la
vostra instància.
oauth2.grant.moderate.post.pin: Fixar les publicacions a la part superior de les
revistes que modereu.
2fa.available_apps: Utilitzar una aplicació d'autenticació de dos factors com
ara %google_authenticator%, %aegis% (Android) o %raivo% (iOS) per escanejar el
codi QR.
front_default_sort: Ordenació predeterminada de la portada
purge_content_desc: Purgar completament el contingut de l'usuari(a), incloent la
supressió de les respostes d'altres usuaris(es) en fils, publicacions i
comentaris creats.
flash_post_new_success: La publicació s'ha creat correctament.
schedule_delete_account: Programar eliminació
2fa.authentication_code.label: Codi d'autenticació
comment_default_sort: Ordenació predeterminada dels comentaris
2fa.setup_error: Error en activar A2F per al compte
2fa.backup-create.help: Podeu crear nous codis d'autenticació de recolzament;
fer-ho invalidarà els codis existents.
subscription_sort: Ordenar
email_application_pending: El vostre compte requerix l'aprovació de
l'administració abans de poder iniciar sessió.
cancel: Cancel·lar
notification_title_removed_post: S'ha eliminat una publicació
password_and_2fa: Contrasenya i A2F
flash_email_failed_to_sent: No s'ha pogut enviar el correu electrònic.
bookmark_remove_from_list: Eliminar el marcador de %list%
flash_comment_edit_error: No s'ha pogut editar el comentari. Alguna cosa ha
fallat.
flash_user_settings_general_success: La configuració d'usuari(a) s'ha desat
correctament.
accept: Acceptar
remove_following: Eliminar el seguiment
sensitive_warning: Contingut sensible
ownership_requests: Sol·licituds de propietat
moderator_requests: Sol·licituds de moderació
notification_title_edited_post: S'ha editat una publicació
deleted_by_moderator: El fil, la publicació o el comentari ha sigut suprimit per
l'equip de moderació
spoiler: Spoiler
hide: Amagar
sso_registrations_enabled.error: Els registres de comptes nous amb gestors
d'identitats de tercers estan actualment desactivats.
compact_view_help: Una vista compacta amb marges menors, on la miniatura passa
al costat dret.
restrict_magazine_creation: Restringir la creació de revistes locals a
l'administració i moderació global
open_signup_request: Obrir la sol·licitud de registre
own_report_accepted: La vostra denúncia ha sigut acceptada
report_accepted: S'ha acceptat una denúncia
magazine_log_mod_removed: ha llevat un(a) moderador(a)
notification_title_new_comment: Nou comentari
magazine_log_entry_pinned: entrada fixada
by: per
direct_message: Missatge directe
manually_approves_followers: Aprova seguidors(es) manualment
register_push_notifications_button: Registreu-vos per a les notificacions «push»
test_push_message: Hola món!
notification_title_message: Nou missatge directe
bookmark_list_create_label: Nom de la llista
notification_title_new_signup: S'ha registrat un nou compte
notification_title_new_report: S'ha creat una nova denúncia
notification_body_new_signup: S'ha registrat el compte %u%.
bookmark_list_make_default: Fer predeterminada
bookmark_lists: Llistes de marcadors
bookmarks: Marcadors
bookmark_list_edit: Editar
bookmark_list_selected_list: Llista seleccionada
bot_body_content: "Benvinguda a l'agent Mbin! Aquest agent té un paper crucial per
habilitar la funcionalitat d'ActivityPub dins de Mbin. Assegura que Mbin es puga
comunicar i federar amb altres instàncies del fedivers.\n\nActivityPub és un protocol
estàndard obert que permet que les plataformes de xarxes socials descentralitzades
es comuniquen i interactuen entre elles. Permet a usuari(e)s de diferents instàncies
(servidors) seguir, interactuar i compartir contingut a través de la xarxa social
federada coneguda com a fedivers. Proporciona una manera estandarditzada per publicar
contingut, seguir altres usuaris(es) i participar en interaccions socials, com ara
fer m'agrada, compartir i comentar fils o publicacions."
delete_content_desc: Suprimir el contingut de l'usuari(a) deixant les respostes
d'altres usuaris(es) als fils, publicacions i comentaris creats.
magazine_posting_restricted_to_mods_warning: Només la moderació pot crear fils
en aquesta revista
new_user_description: Aquest compte és nou (actiu durant menys de %days% dies)
new_users_need_approval: Els comptes nous han de ser aprovats per
l'administració abans que puguen iniciar sessió.
new_magazine_description: Aquesta revista és nova (activa durant menys de %days%
dies)
admin_users_inactive: Inactius(ves)
bookmarks_list: Marcadors en %list%
table_of_contents: Taula de continguts
email_verification_pending: Heu de verificar la vostra adreça electrònica abans
de poder iniciar sessió.
search_type_all: Tot
flash_application_info: L'administració ha d'aprovar el vostre compte abans de
poder iniciar sessió. Rebreu un correu electrònic quan s'haja processat la
vostra sol·licitud de registre.
account_banned: El compte ha sigut vetat.
answered: respost
own_report_rejected: La vostra denúncia ha sigut rebutjada
2fa.manual_code_hint: Si no podeu escanejar el codi QR, introduïu el secret
manualment
toolbar.emoji: Emoji
magazine_instance_defederated_info: La instància d'esta revista està
desfederada. Per tant, la revista no rebrà actualitzacions.
user_instance_defederated_info: La instància d'este compte està defederada.
flash_thread_instance_banned: La instància d'esta revista està banejada.
show_rich_mention: Mencions enriquides
show_rich_mention_help: Mostrar un component de compte quan es menciona un
compte. Això n'inclourà el nom de visualització i la foto de perfil.
show_rich_mention_magazine: Mencions enriquides de revistes
show_rich_mention_magazine_help: Mostrar un component de revista quan es
menciona una revista. Això in'nclourà el nom de visualització i la icona.
show_rich_ap_link: Enllaços AP enriquits
show_rich_ap_link_help: Mostrar un component en línia quan s'hi enllaça un altre
contingut d'ActivityPub.
attitude: Actitud
type_search_term_url_handle: Escriviu el terme de cerca, l'URL o l'identificador
search_type_magazine: Revistes
search_type_user: Comptes
search_type_actors: Revistes i comptes
search_type_content: Temes i microblogs
type_search_magazine: Limitar la cerca a la revista...
type_search_user: Limitar la cerca a l'autoria...
modlog_type_entry_deleted: Fil suprimit
modlog_type_entry_restored: Fil restaurat
modlog_type_entry_comment_deleted: Comentari del fil suprimit
modlog_type_entry_comment_restored: Comentari del fil restaurat
modlog_type_entry_pinned: Fil fixat
modlog_type_entry_unpinned: Fil deixat de fixar
modlog_type_post_deleted: Microblog suprimit
modlog_type_post_restored: Microblog restaurat
modlog_type_post_comment_deleted: Resposta del microblog suprimida
modlog_type_post_comment_restored: Resposta del microblog restaurada
modlog_type_ban: Compte expulsat de la revista
modlog_type_moderator_add: Moderador(a) de la revista afegit(da)
modlog_type_moderator_remove: Moderador(a) de la revista destituït(da)
everyone: Tot el món
nobody: Ningú
followers_only: Només seguidor(e)s
direct_message_setting_label: Qui pot enviar-vos un missatge directe
banner: Bàner
magazine_theme_appearance_banner: Bàner personalitzat per a la revista. Es
mostra damunt de tots els fils de discussió i ha de tindre una relació
d'aspecte ampla (5:1 ó 1500 px * 300 px).
delete_magazine_icon: Suprimix icona de la revista
flash_magazine_theme_icon_detached_success: La icona de la revista s'ha suprimit
correctament
delete_magazine_banner: Suprimix el bàner de la revista
flash_magazine_theme_banner_detached_success: El bàner de la revista s'ha
suprimit correctament
crosspost: Publicació creuada
flash_thread_ref_image_not_found: No s'ha pogut trobar la imatge a què fa
referència 'imageHash'.
federation_uses_allowlist: Utilitzar la llista de permesos per a la federació
defederating_instance: S'està defederant la instància %i
their_user_follows: Quantitat de comptes de la seua instància que seguixen
comptes de la nostra
our_user_follows: Quantitat de comptes de la nostra instància que seguixen
comptes de la seua
their_magazine_subscriptions: Quantitat de comptes de la seua instància
subscrits a revistes de la nostra
our_magazine_subscriptions: Quantitat de comptes de la nostra instància
subscrits a revistes des de la seua
confirm_defederation: Confirmar la desfederació
flash_error_defederation_must_confirm: Heu de confirmar la desfederació
allowed_instances: Instàncies permeses
btn_deny: Denegar
btn_allow: Permetre
ban_instance: Prohibir instància
allow_instance: Permetre instància
federation_page_use_allowlist_help: Si s'utilitza una llista de permesos, esta
instància només es federarà amb les instàncies explícitament permeses. En cas
contrari, esta instància es federarà amb totes les instàncies, excepte les que
estiguen prohibides.
front_default_content: Vista per defecte de portada
default_content_default: Valor per defecte del servidor (Fils)
default_content_threads: Fils
default_content_microblog: Microblog
combined: Combinat
sidebar_sections_random_local_only: Restringir les seccions de la barra lateral
«Publicacions/Fils aleatoris» només a locals
sidebar_sections_users_local_only: Restringir la secció de la barra lateral
«Persones actives» només a locals
random_local_only_performance_warning: Habilitar «Només aleatoris locals» pot
afectar el rendiment de l'SQL.
default_content_combined: Fils + Microblog
ban_expires: La prohibició caduca
you_have_been_banned_from_magazine: Vos han prohibit l'accés a la revista %m.
you_have_been_banned_from_magazine_permanently: Vos han prohibit permanentment
l'accés a la revista %m.
you_are_no_longer_banned_from_magazine: Ja no teniu prohibit l'accés a la
revista %m.
oauth2.grant.moderate.entry.lock: Bloqueja els fils de les revistes moderades
perquè ningú no hi puga fer comentaris
oauth2.grant.moderate.post.lock: Bloqueja els microblogs a les revistes
moderades, perquè ningú no hi puga fer comentaris
discoverable: Descobrible
user_discoverable_help: Si esta opció està habilitada, el vostre perfil, fils de
discussió, microblogs i comentaris es poden trobar mitjançant la cerca i els
panells aleatoris. El vostre perfil també pot aparèixer al panell d'usuari(a)
actiu(va) i a la pàgina de persones. Si esta opció està desactivada, les
vostres publicacions continuaran sent visibles per a altres usuari(e)s, però
no apareixeran al canal complet.
magazine_discoverable_help: Si això està habilitat, esta revista i els fils,
microblogs i comentaris d'esta revista es poden trobar mitjançant la busca i
els panells aleatoris. Si això està desactivat, la revista encara apareixerà a
la llista de revistes, però els fils i microblogs no apareixeran al canal
complet.
flash_thread_lock_success: Fil bloquejat correctament
flash_thread_unlock_success: Fil desbloquejat correctament
flash_post_lock_success: Microblog bloquejat correctament
flash_post_unlock_success: Microblog desbloquejat correctament
lock: Bloquejar
unlock: Desbloquejar
comments_locked: Els comentaris estan bloquejats.
magazine_log_entry_locked: ha bloquejat els comentaris de
magazine_log_entry_unlocked: ha desbloquejat els comentaris de
modlog_type_entry_lock: Fil bloquejat
modlog_type_entry_unlock: Fil desbloquejat
modlog_type_post_lock: Microblog bloquejat
modlog_type_post_unlock: Microblog desbloquejat
contentnotification.muted: Silenciós | no rebre notificacions
contentnotification.default: Predeterminat | rebre notificacions segons la
configuració predeterminada
contentnotification.loud: Sorollós | rebre totes les notificacions
indexable_by_search_engines: Indexable pels motors de cerca
user_indexable_by_search_engines_help: Si esta configuració es desactiva, es
recomana als motors de cerca que no indexen cap dels vostres fils i
microblogs, però els vostres comentaris no es veuen afectats per això i els
malfactors podrien ignorar-la. Esta configuració també està federada a altres
servidors.
magazine_indexable_by_search_engines_help: Si esta configuració es desactiva, es
recomana als motors de cerca que no indexen cap dels fils i microblogs d'estes
revistes. Açò inclou la pàgina de destinació i totes les pàgines de
comentaris. Esta configuració també està federada a d'altres servidors.
magazine_name_as_tag: Empra el nom de la revista com a etiqueta
magazine_name_as_tag_help: Les etiquetes d'una revista s'utilitzen per fer
coincidir les entrades de microblog amb esta revista. Per exemple, si el nom
és "fediverse" i les etiquetes de la revista contenen "fediverse", totes les
entrades de microblog que continguen "#fediverse" es posaran en esta revista.
magazine_rules_deprecated: el camp de regles està obsolet i s'eliminarà en el
futur. Per favor, poseu les vostres regles al quadre de descripció.
created_since: Creat des de
================================================
FILE: translations/messages.da.yaml
================================================
type.link: Link
type.article: Tråd
type.photo: Foto
type.video: Video
type.smart_contract: intelligent kontrakt
type.magazine: Magasin
thread: Tråd
people: Folk
events: Begivenheder
magazine: Magasin
magazines: Magasiner
search: Søg
select_channel: Vælge en kanal
login: Log ind
hot: Heftig
active: Aktiv
newest: Nyeste
oldest: Ældste
commented: Kommenterede
filter_by_time: Filtrer efter tid
filter_by_type: Filtrer efter type
comments_count: '{0}Kommentarer|{1}Kommentar|]1,Inf[ Kommentarer'
subscribers_count: '{0}Abonnenter|{1}Abonnent|]1,Inf[Abonnenter'
followers_count: '{0}Følgere|{1}Følger|]1,Inf[ Følgere'
marked_for_deletion: Markeret til sletning
marked_for_deletion_at: markeret til sletning d. %date%
favourites: Favoritter
favourite: Favorit
more: Mere
added: Tilføjet
up_votes: Booster
threads: Tråde
microblog: Mikroblog
add: Tilføj
top: Top
change_view: Ændre visning
no_comments: Ingen kommentarer
created_at: Oprettet
avatar: Avatar
================================================
FILE: translations/messages.de.yaml
================================================
type.article: Thema
type.photo: Foto
type.video: Video
type.magazine: Magazin
people: Personen
magazine: Magazin
magazines: Magazine
search: Suchen
add: Hinzufügen
change_view: Ansicht wechseln
created_at: Erstellt
filter_by_type: Filtern nach Typ
active: Aktiv
select_channel: Kanal auswählen
events: Ereignisse
login: Anmelden
newest: Neu
oldest: Alt
commented: kommentiert
favourites: Upvotes
favourite: Favorit
more: Mehr
avatar: Avatar
added: Hinzugefügt
subscribers: 'Abonnenten'
comments: Kommentare
cards_view: Karten-Ansicht
12h: 12h
3h: 3h
6h: 6h
1m: 1m
1w: 1w
rules: Regeln
edited_post: Beitrag wurde bearbeitet
Password is invalid: Passwort ist ungültig.
Your account has been banned: Dein Benutzerkonto ist gesperrt.
firstname: Vorname
filter_by_time: Filtern nach Zeit
comments_count: '{0}Kommentare|{1}Kommentar|]1,Inf[ Kommentare'
comment: Kommentar
table_view: Tabellen-Ansicht
terms: AGB
profile: Profil
article: Thema
type.link: Link
thread: Thema
no_comments: Keine Kommentare
owner: Eigentümer
threads: Themen
microblog: Mikroblog
enter_your_comment: Gib deinen Kommentar ein
enter_your_post: Gib deinen Beitrag ein
cover: Banner
remember_me: An mich erinnern
dont_have_account: Noch kein Konto?
you_cant_login: Passwort vergessen?
add_new_photo: Neues Foto erstellen
add_new_post: Neuen Beitrag erstellen
add_new_video: Neues Video erstellen
privacy_policy: Datenschutzbestimmungen
useful: Nützlich
edit: Bearbeiten
delete: Löschen
edit_post: Beitrag bearbeiten
edit_comment: Änderungen speichern
new_password_repeat: Neues Passwort bestätigen
font_size: Schriftgröße
no: Nein
show_all: Alle anzeigen
already_have_account: Hast du bereits ein Konto?
new_password: Neues Passwort
yes: Ja
online: Online
replies: Antworten
moderators: Moderatoren
add_comment: Kommentar hinzufügen
add_media: Medien hinzufügen
activity: Aktivität
federated_magazine_info: Dieses Magazin ist von einem föderierten Server und
möglicherweise unvollständig.
federated_user_info: Dieses Profil ist von einem föderierten Server und
möglicherweise unvollständig.
empty: Leer
subscribe: Abonnieren
follow: Folgen
password: Passwort
related_posts: Ähnliche Beiträge
markdown_howto: Wie funktioniert der Editor?
unsubscribe: Abo beenden
register: Registrieren
reset_password: Passwort zurücksetzen
username: Benutzername
email: E-Mail
repeat_password: Passwort wiederholen
posts: Beiträge
top: Top
mod_log: Moderations-Log
add_post: Beitrag hinzufügen
random_posts: Zufällige Beiträge
login_or_email: Login oder E-Mail
reply: Antworten
show_more: Mehr anzeigen
to: an
in: in
all_magazines: Alle Magazine
stats: Statistiken
fediverse: Fediverse
add_new_link: Neuen Link erstellen
create_new_magazine: Neues Magazin erstellen
contact: Kontakt
faq: FAQ
rss: RSS
help: Hilfe
check_email: Prüfe deine E-Mails
reset_check_email_desc2: Bitte prüfe deinen Spam-Ordner, wenn du keine E-Mail
bekommen hast.
try_again: Nochmal versuchen
email_confirm_header: Hallo! Bitte bestätige deine E-Mail-Adresse.
email_verify: E-Mail-Adresse bestätigen
email_confirm_expire: Achtung, dieser Link wird in einer Stunde ablaufen.
email_confirm_title: Bestätige deine E-Mail-Adresse.
select_magazine: Wähle ein Magazin
url: URL
title: Titel
eng: ENG
image: Bild
image_alt: Alternativtext zum Bild
name: Name
description: Beschreibung
domain: Domäne
overview: Übersicht
cards: Karten
columns: Spalten
user: Nutzer
moderated: Moderiert
people_local: Lokal
reputation_points: Reputationspunkte
go_to_content: Zum Inhalt springen
go_to_filters: Zu den Filtern springen
go_to_search: Zur Suche springen
all: Alle
logout: Abmelden
classic_view: Klassische Ansicht
compact_view: Kompakte Ansicht
general: Allgemein
dynamic_lists: Dynamische Listen
kbin_intro_title: Das Fediverse erkunden
kbin_promo_title: Eigene Instanz erstellen
captcha_enabled: Captcha aktiviert
return: Zurück
about_instance: Über
add_new_article: Neues Thema erstellen
reset_check_email_desc: Wenn zu dieser Email-Addresse bereits ein Konto
registriert ist, dann wird in Kürze eine E-Mail mit einem Link um das Passwort
zurückzusetzen gesendet. Dieser Link läuft in %expire% ab.
email_confirm_content: 'Bereit dein Mbin-Konto zu aktivieren? Dann klicke den folgenden
Link:'
agree_terms: Stimme den %terms_link_start%Allgemeine
Geschäftsbedingungen%terms_link_end% und der
%policy_link_start%Datenschutzerklärung%policy_link_end% zu
up_votes: Boosts
down_votes: Reduziert
users: Benutzer
note: Notiz
reputation: Ruf
preview: Vorschau
pinned: Angeheftet
change: Ändern
change_magazine: Magazin ändern
unpin: Lösen
pin: Anheften
done: Fertig
trash: Papierkorb
created: Erstellt
rejected: Abgelehnt
filters: Filter
reject: Ablehnen
pages: Seiten
instance: Instanz
meta: Meta
dashboard: Dashboard
local: Lokal
federated: Föderiert
year: Jahr
months: Monate
month: Monat
weeks: Wochen
week: Woche
content: Inhalt
change_language: Sprache ändern
type.smart_contract: Smart Contract
hot: Heiß
unfollow: Entfolgen
subscribed: Abonniert
subscriptions: Abonnements
chat_view: Chat-Ansicht
are_you_sure: Bist du dir sicher?
moderate: Moderieren
reason: Grund
settings: Einstellungen
notifications: Benachrichtigungen
messages: Nachrichten
privacy: Datenschutz
notify_on_new_entry_reply: Alle Kommentare in meinen erstellten Themen
notify_on_new_entry_comment_reply: Antworten zu meinen Kommentaren in allen
Themen
notify_on_new_post_comment_reply: Antworten zu meinen Kommentaren in Beiträgen
notify_on_new_entry: Neue Themen (Links und Artikel) in sämtlichen Magazinen die
ich abonniert habe
save: Speichern
notify_on_new_post_reply: Antworten aller Ebenen zu Beiträgen die ich verfasst
habe
notify_on_new_posts: Neue Beiträge in sämtlichen Magazinen die ich abonniert
habe
change_theme: Design ändern
add_new: Erstellen
badges: Abzeichen
body: Inhalt
is_adult: 18+ / NSFW
joined: Beigetreten
following: Folgende
1d: 1t
1y: 1j
links: Links
articles: Themen
photos: Bilder
videos: Videos
report: Melden
share: Teilen
copy_url: Mbin URL kopieren
share_on_fediverse: Im Fediverse teilen
go_to_original_instance: Auf der Original-Instanz anzeigen
up_vote: Boost
down_vote: Reduzieren
tags: Tags
followers: Follower
copy_url_to_fediverse: Original-URL kopieren
blocked: Geblockt
reports: Meldungen
oc: OC
people_federated: Föderiert
tree_view: Strukturansicht
related_tags: Verwandte Tags
appearance: Design
homepage: Startseite
hide_adult: 18+ Inhalte verbergen
featured_magazines: Vorgestellte Magazine
show_profile_subscriptions: Magazinabos anzeigen
about: Über
old_email: Derzeitige E-Mail
new_email: Neue E-Mail
new_email_repeat: Neue E-Mail bestätigen
current_password: Aktuelles Passwort
show_profile_followings: Folgende Nutzer anzeigen
change_email: E-Mail ändern
change_password: Passwort ändern
expand: Erweitern
collapse: Einklappen
error: Fehler
votes: Stimmen
theme: Design
solarized_light: Sonniges Hell
solarized_dark: Sonniges Dunkel
boosts: Boosts
show_users_avatars: Nutzeravatare anzeigen
show_magazines_icons: Magazin-Icons anzeigen
rounded_edges: Abgerundete Ecken
removed_post_by: hat den Beitrag entfernt, erstellt von
restored_post_by: hat den Beitrag wiederhergestellt, erstellt von
he_banned: sperrte
he_unbanned: entsperrte
domains: Domänen
dark: Dunkel
light: Hell
size: Größe
show_thumbnails: Vorschaubilder anzeigen
read_all: Alle als gelesen markieren
flash_register_success: Willkommen an Bord! Dein Konto wurde registriert. Ein
letzter Schritt — überprüfe deinen Posteingang nach einem Aktivierungslink der
dein Konto zum Leben erwecken wird.
flash_thread_new_success: Das Thema wurde erfolgreich erstellt und ist jetzt für
andere Benutzer sichtbar.
related_magazines: Verwandte Magazine
active_users: Aktive Personen
sidebar: Seitenleiste
banned_instances: Gesperrte Instanzen
header_logo: Header Logo
flash_thread_edit_success: Das Thema wurde erfolgreich bearbeitet.
flash_thread_delete_success: Das Thema wurde erfolgreich gelöscht.
flash_thread_pin_success: Das Thema wurde erfolgreich angeheftet.
flash_thread_unpin_success: Das Thema wurde erfolgreich losgeheftet.
flash_magazine_edit_success: Das Magazin wurde erfolgreich bearbeitet.
too_many_requests: Grenzwert überschritten, bitte versuche es später nochmal.
set_magazines_bar_desc: Füge die Namen der Magazine hinter dem Komma ein
set_magazines_bar_empty_desc: Wenn das Feld leer ist werden aktive Magazine auf
der Magazin-Leiste angezeigt.
set_magazines_bar: Magazin-Leiste
added_new_thread: Ein neues Thema wurde hinzugefügt
mod_deleted_your_comment: Ein Moderator hat deinen Kommentar entfernt
mod_remove_your_post: Ein Moderator hat deinen Beitrag entfernt
removed: Entfernt von einem Moderator
deleted: Vom Author gelöscht
post: Beitrag
send_message: Direktnachricht senden
message: Nachricht
left: Links
right: Rechts
status: Status
on: An
off: Aus
instances: Instanzen
upload_file: Datei hochladen
from_url: Von Url
add_moderator: Moderator hinzufügen
add_badge: 'Abzeichen hinzufügen'
expires: Läuft ab
FAQ: FAQ
type_search_term: Suchbegriff eingeben
registrations_enabled: 'Registrierung aktiviert'
restore: Wiederherstellen
send: Senden
Your account is not active: Dein Benutzerkonto ist nicht aktiv.
random_magazines: Zufällige Magazine
random_entries: Zufällige Themen
related_entries: Verwandte Themen
delete_account: Konto löschen
auto_preview: Automatische Medienvorschau
mod_remove_your_thread: Ein Moderator hat dein Thema entfernt
mod_log_alert: WARNUNG - Der Modlog kann unangenehme oder verstörende Inhalte
enthalten, welche von Moderatoren entfernt wurden. Bitte bedenke was du tust.
removed_thread_by: hat ein Thema entfernt, erstellt von
restored_thread_by: hat ein Thema wiederhergestellt, erstellt von
removed_comment_by: hat einen Kommentar entfernt, erstellt von
restored_comment_by: hat den Kommentar wiederhergestellt, erstellt von
flash_magazine_new_success: Das Magazin wurde erfolgreich erstellt. Du kannst
nun neue Inhalte hinzufügen oder die Administrationsansicht des Magazins
öffnen.
edited_thread: Thema wurde bearbeitet
added_new_comment: Neuer Kommentar hinzugefügt
edited_comment: Kommentar bearbeitet
added_new_post: Neuen Beitrag hinzugefügt
added_new_reply: Antwort wurde hinzugefügt
wrote_message: Schrieb eine Nachricht
banned: Hat dich gesperrt
mentioned_you: Hat dich erwähnt
subject_reported: Beitrag wurde gemeldet.
sidebar_position: Position der Seitenleiste
federation: Föderation
approve: Genehmigen
approved: Genehmigt
perm: Dauerhaft
contact_email: Kontakt-Mailadresse
replied_to_your_comment: Antwortete auf deinen Kommentar
purge: Aufräumen
infinite_scroll: Unendliches Scrollen
show_top_bar: Zeige obere Statusleiste an
expired_at: Abgelaufen am
registration_disabled: Registrierung geschlossen
federation_enabled: Föderation aktiviert
purge_account: Konto endgültig löschen
unban_account: Konto entsperren
kbin_intro_desc: ist eine dezentralisierte Plattform zur Inhaltssammlung und für
Mikroblogs im Fediverse.
magazine_panel_tags_info: Nur benutzen wenn du möchtest, dass Inhalte aus dem
Fediverse, basierend auf Tags, in dieses Magazin eingefügt werden
sticky_navbar: Statische Navigationsleiste
kbin_promo_desc: '%link_start%Klone das Repo%link_end% und erweitere das Fediverse'
browsing_one_thread: Du siehst nur Inhalte aus einem Thema in dieser Diskussion.
Alle Kommentare sind auf der Beitragsseite verfügbar.
boost: Boost
icon: Icon
magazine_panel: Magazin Panel
ban: Sperre
bans: Sperren
add_ban: Sperre hinzufügen
writing: Verfassen
admin_panel: Admin Panel
add_mentions_posts: Erwähnungen in Beiträgen hinzufügen
ban_account: Konto sperren
add_mentions_entries: Erwähnungen in Themen hinzufügen
ban_expired: Sperre abgelaufen
mercure_enabled: Mercure aktiviert
report_issue: Fehler melden
tokyo_night: Tokyo Night
preferred_languages: Sprachen von Themen und Beiträgen filtern
infinite_scroll_help: Automatisch mehr Inhalte laden, wenn du das Ende der Seite
erreichst.
sticky_navbar_help: Die Navigationsleiste wird oben an der Seite angeheftet
bleiben während du nach unten scrollst.
auto_preview_help: Zeige die Medien-Vorschau (Bild, Video) vergrößert unter dem
Inhalt.
reload_to_apply: Seite neu laden, um Änderungen anzuwenden
filter.origin.label: Ursprung auswählen
filter.adult.label: Wähle ob NSFW soll gezeigt werden
filter.adult.hide: NSFW verbergen
filter.adult.show: NSFW anzeigen
filter.adult.only: Nur NSFW
local_and_federated: Lokal und föderiert
filter.fields.only_names: Nur Namen
filter.fields.names_and_descriptions: Namen und Beschreibungen
kbin_bot: Mbin Agent
filter.fields.label: Wähle Felder für die Suche
bot_body_content: "Willkommen beim Mbin Agenten! Der Agent spielt eine entscheidende
Rolle um die ActivityPub-Funktionalität für Mbin bereitzustellen. Er stellt sicher,
dass Mbin mit anderen Instanzen des Fediverse kommunizieren und föderieren kann.\n\
\ \nActivityPub ist ein offenes Protokoll dass dezentralen sozialen Netzwerken ermöglicht
miteinander zu kommunizieren und zu interagieren. Es ermöglicht Nutzern auf unterschiedlichen
Instanzen (Servern) anderen Nutzern zu folgen, mit ihnen zu interagieren und Inhalte
innerhalb des föderierten sozialen Netzwerk, bekannt als das Fediverse zu teilen.
Es stellt eine standardisierte Schnittstelle bereit über die Nutzer Inhalte veröffentlichen,
anderen Nutzern folgen, und mit ihnen interagieren können indem sie Inhalte favorisieren,
teilen oder kommentieren."
password_confirm_header: Bestätige deine Passwortänderungsanfrage.
toolbar.bold: Fett
toolbar.italic: Kursiv
toolbar.strikethrough: Durchgestrichen
toolbar.header: Überschrift
toolbar.quote: Zitat
toolbar.code: Code
toolbar.link: Link
toolbar.image: Bild
toolbar.unordered_list: Liste
toolbar.mention: Erwähnen
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: 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
federated_search_only_loggedin: Föderierte Suche ist eingeschränkt wenn nicht
angemeldet
more_from_domain: Mehr von der Domäne
resend_account_activation_email_error: Es gab ein Problem bei der Bearbeitung
der Anfrage. Eventuell gibt es keinen Account der mit diese E-Mail assoziiert
wird oder vielleicht ist der Account bereits aktiviert.
email_confirm_button_text: Bestätige deine Passwortänderungsanfrage
errors.server429.title: 429 Zu viele Anfragen
oauth.consent.to_allow_access: Um den Zugriff zu gestatten, klicken Sie den
"Erlauben" Button weiter unten
email.delete.description: Der folgende Nutzer hat die Löschung seines Kontos
angefragt
oauth.consent.app_requesting_permissions: möchte die folgenden Aktionen in
deinem Namen ausführen
oauth.consent.allow: Erlauben
custom_css: Eigenes CSS
block: Blockieren
oauth2.grant.moderate.magazine.list: Lese eine Liste deine moderierten Magazine.
errors.server404.title: 404 Nicht Gefunden
resend_account_activation_email_success: Wenn es einen Account mit dieser E-Mail
gibt, werden wir eine neue Aktivierungsmail senden.
errors.server403.title: 403 Zugriff Verboten
ignore_magazines_custom_css: Ignoriere das eigene CSS eines Magazins
sidebars_same_side: Seitenleisten auf der selben Seite
oauth.consent.deny: Verbieten
oauth.consent.title: OAUTH2 Zustimmungsformular
resend_account_activation_email: Konto Aktivierungsmail erneut senden
errors.server500.title: 500 Interner Serverfehler
alphabetically: Alphabetisch
show_subscriptions: Zeige Abonnements
resend_account_activation_email_question: Inaktiver Account?
resend_account_activation_email_description: Gib die E-Mail-Adresse die mit
deinem Account assoziiert ist ein. Wir werden erneut eine Aktivierungsmail für
Sie senden.
errors.server500.description: Es tut uns leid, etwas ist schiefgelaufen. Sollte
dieser Fehler bestehen kontaktieren Sie bitte den Instanz Besitzer. Wenn diese
Instanz überhaupt nicht funktioniert kann eine %link_start%andere Mbin
Instanz%link_end% verwendet werden bis das Problem gelöst ist.
oauth.client_not_granted_message_read_permission: Diese App hat keine
Berechtigung erhalten deine Nachrichten zu lesen.
restrict_oauth_clients: Beschränke OAuth2 Client Erstellung auf Admins
subscription_sort: Sortierung
unblock: Entblockieren
oauth.consent.grant_permissions: Gebe Berechtigungen
oauth2.grant.moderate.magazine.ban.delete: Nutzer in deinen moderierten
Magazinen 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
subscription_sidebar_pop_in: In andere Seitenleiste verschieben
subscription_panel_large: Großes Panel
subscription_header: Abonnierte Magazine
oauth.client_identifier.invalid: Ungültige OAuth Client ID!
oauth.consent.app_has_permissions: kann bereits die folgenden Aktionen ausführen
email.delete.title: Benutzerkonto Löschanfrage
email_confirm_link_help: Alternativ können Sie das Folgende in ihren Browser
kopieren
2fa.authentication_code.label: Authentisierungscode
oauth2.grant.post.edit: Bearbeite existierende Beiträge.
oauth2.grant.moderate.post.trash: Lösche oder stelle Beiträge in deinen
moderierten Magazinen wieder her.
moderation.report.approve_report_title: Meldung Zustimmen
moderation.report.reject_report_title: Meldung Ablehnen
oauth2.grant.moderate.magazine.reports.all: Bearbeite Meldungen in deinen
moderierten Magazinen.
oauth2.grant.admin.federation.update: Instanzen der liste der deföderierten
Instanzen hinzufügen oder davon entfernen.
oauth2.grant.user.message.all: Lese deine Nachrichten und sende Nachrichten an
andere Nutzer.
oauth2.grant.moderate.magazine.trash.read: Gelöschte Inhalte in Ihren
moderierten Magazinen sehen.
oauth2.grant.moderate.magazine_admin.create: Erstelle neue Magazine.
oauth2.grant.post.vote: Favorisiere, booste oder reduziere jeden Beitrag.
2fa.remove: 2FA Entfernen
delete_content_desc: Lösche den Inhalt des Nutzers, aber behalte die Antworten
anderer Nutzer in den erstellten Themen, Beiträgen und Kommentaren.
magazine_theme_appearance_custom_css: Eigenes CSS das angewendet wird wenn
Inhalte innerhalb deines Magazins angeschaut werden.
2fa.backup_codes.help: Du kannst diese Code nutzen wenn du keinen Zugriff auf
dein Zwei-Faktor-Authentifizierung Gerät oder deine App hast. Die Codes werden
dir nicht noch einmal gezeigt werden und du kannst jeden Code
nur ein einziges Mal nutzen.
oauth2.grant.user.oauth_clients.edit: Bearbeite die Berechtigungen die du
anderen OAuth2 Apps gegeben hast.
oauth2.grant.user.all: Lese und bearbeite dein Profil, deine Nachrichten und
Benachrichtigungen; Lese und bearbeite Berechtigungen die du anderen Apps
gegeben hast; Folge oder blockiere andere Nutzer; Zeige die Nutzer denen du
folgst oder die du blockiert hast.
oauth2.grant.moderate.post.set_adult: Kennzeichne Beiträge in deinen moderierten
Magazinen als NSFW.
oauth2.grant.moderate.magazine_admin.edit_theme: Bearbeite das Custom CSS deiner
eigenen Magazine.
oauth2.grant.moderate.magazine_admin.tags: Erstelle oder Entferne Tags von
deinen eigenen Magazinen.
moderation.report.ban_user_description: Willst du den Nutzer (%username%)
sperren der diesen Inhalt von dem Magazin erstellt hat?
oauth2.grant.moderate.entry.pin: Pinne Themen an den Anfang deiner moderierten
Magazine.
oauth2.grant.user.message.read: Lese deine Nachrichten.
oauth2.grant.admin.entry.purge: Lösche jedes Thema endgültig von deiner Instanz.
2fa.verify: Verifizieren
2fa.add: Zu meinem Konto hinzufügen
oauth2.grant.admin.magazine.all: Verschiebe Themen zwischen Magazinen oder
lösche Magazine endgültig von deiner Instanz.
oauth2.grant.admin.instance.settings.read: Zeige die Einstellungen deiner
Instanz.
oauth2.grant.entry.report: Melde jedes Thema.
oauth2.grant.moderate.post_comment.all: Moderiere Kommentare unter Beiträgen in
deinen moderierten Magazinen.
2fa.disable: Zwei-Faktor-Authentifizierung Deaktivieren
last_active: Zuletzt Aktiv
purge_content: Lösche Inhalt endgültig
oauth2.grant.domain.subscribe: Abonniere oder Deabonniere Domains und zeige die
Domains die du abonniert hast.
magazine_theme_appearance_icon: Benutzerdefiniertes Icon für das Magazin.
oauth2.grant.moderate.magazine.ban.create: Sperre Nutzer in deinen moderierten
Magazinen.
oauth2.grant.admin.user.delete: Lösche Nutzer von deiner Instanz.
oauth2.grant.moderate.post.change_language: Ändere die Sprache von Beiträgen in
deinen moderierten Magazinen.
oauth2.grant.moderate.magazine_admin.delete: Lösche alle deine eigenen Magazine.
oauth2.grant.moderate.entry_comment.all: Moderiere Kommentare in Themen in
deinen moderierten Magazinen.
oauth2.grant.admin.magazine.move_entry: Verschiebe Themen zwischen Magazinen auf
deine Instanz.
oauth2.grant.post_comment.edit: Bearbeite deine existierenden Kommentare unter
Beiträgen.
oauth2.grant.moderate.post.pin: Hefte Beiträge oben an deine moderierten
Magazine an.
oauth2.grant.entry.create: Erstelle neue Themen.
oauth2.grant.moderate.magazine.reports.action: Akzeptiere oder Verwerfe
Meldungen in deinen moderierten Magazinen.
oauth2.grant.admin.magazine.purge: Lösche Magazine endgültig von deiner Instanz.
oauth2.grant.user.notification.read: Lese deine Benachrichtigungen, inklusive
Benachrichtigungen zu Nachrichten.
oauth2.grant.magazine.block: Blockiere oder entblockiere Magazine und zeige
Magazine die du blockiert hast.
oauth2.grant.magazine.subscribe: Abonniere oder deabonniere Magazine und zeige
die Magazine die du abonniert hast.
oauth2.grant.admin.user.all: Sperre, verifiziere oder lösche Nutzer endgültig
von deiner Instanz.
2fa.backup_codes.recommendation: Es wird empfohlen dass du eine Kopie davon an
einem sicheren Ort aufbewahrst.
oauth2.grant.post_comment.report: Melde jeden Kommentar unter einem Beitrag.
oauth2.grant.magazine.all: Abonniere oder blockiere Magazine und zeige die
Magazine die du abonniert oder blockiert hast.
oauth2.grant.vote.general: Favorisiere, Reduziere oder Booste jedes Thema,
Betrag oder Kommentar.
oauth2.grant.entry.vote: Favorisiere, booste oder reduziere jedes Thema.
oauth2.grant.moderate.magazine_admin.all: Erstelle, Bearbeite oder Lösche deine
eigenen Magazine.
comment_reply_position_help: Zeige das Kommentar-Formular entweder oben oder
unten auf der Seite. Falls "unendliches scrollen" aktiviert ist wird das
Formular immer oben auf der Seite angezeigt werden.
oauth2.grant.post_comment.vote: Favorisiere, booste oder reduziere jeden
Kommentar unter einem Beitrag.
oauth2.grant.user.oauth_clients.read: Lese die Berechtigungen die du anderen
OAuth2 Apps gegeben hast.
oauth2.grant.moderate.entry.change_language: Ändere die Sprache von Themen in
deinen moderierten Magazinen.
oauth2.grant.moderate.all: Führe jede Moderationshandlung aus zu der du die
Berechtigung in deinen moderierten Magazinen hast.
oauth2.grant.moderate.magazine.ban.all: Verwalte gesperrte Nutzer in deinen
moderierten Magazinen.
oauth2.grant.moderate.magazine.all: Verwalte Sperren, Meldungen und zeige
gelöschte Items in deinen moderierten Magazinen.
oauth2.grant.admin.federation.all: Zeige und aktualisiere die aktuell
deföderierten Instanzen.
oauth2.grant.user.notification.all: Lese und lösche deine Benachrichtigungen.
oauth2.grant.report.general: Melde Themen, Beiträge oder Kommentare.
oauth2.grant.admin.post.purge: Lösche jeden Beitrag endgültig von deiner
Instanz.
oauth2.grant.moderate.magazine.ban.read: Zeige gesperrte Nutzer in deinen
moderierten Magazinen.
oauth2.grant.user.profile.all: Lese und bearbeite dein Profil.
oauth2.grant.admin.user.ban: Sperre oder entsperre Nutzer von deiner Instanz.
show_avatars_on_comments: Zeige Kommentar Avatare
oauth2.grant.admin.all: Führe jede administrative Aktion auf deiner Instanz aus.
oauth2.grant.post.report: Melde jeden Beitrag.
oauth2.grant.moderate.magazine.reports.read: Lese Meldungen in deinen
moderierten Magazinen.
oauth2.grant.entry_comment.create: Erstelle neue Kommentare in Themen.
2fa.qr_code_img.alt: Ein QR-Code der die Einrichtung der
Zwei-Faktor-Authentifizierung für dein Konto ermöglicht
oauth2.grant.user.follow: Folge oder entfolge Nutzern und zeige die Nutzer denen
du folgst.
flash_post_pin_success: Der Beitrag wurde erfolgreich angeheftet.
oauth2.grant.entry_comment.report: Melde jeden Kommentar in einem Thema.
moderation.report.approve_report_confirmation: Bist du dir sicher dass du dieser
Meldung zustimmen willst?
oauth2.grant.moderate.entry.all: Moderiere Themen in deinen moderierten
Magazinen.
oauth2.grant.entry_comment.all: Erstelle, Bearbeite oder Lösche deine Kommentare
in Themen und favorisiere, booste oder melde jeden Kommentar in einem Thema.
oauth2.grant.entry_comment.delete: Lösche deine existierenden Kommentare in
Themen.
delete_account_desc: Lösche das Konto, inklusive Antworten anderer Nutzer in den
erstellten Themen, Beiträgen und Kommentaren.
subject_reported_exists: Dieser Inhalt wurde bereits gemeldet.
oauth2.grant.moderate.entry_comment.set_adult: Kennzeichne Kommentare in Themen
in deinen moderierten Magazinen als NSFW.
oauth2.grant.entry.edit: Bearbeite deine existierenden Themen.
oauth2.grant.moderate.entry_comment.trash: Lösche oder stelle Kommentare in
Themen deiner moderierten Magazine wieder her.
oauth2.grant.moderate.post.all: Moderiere Beiträge in deinen moderierten
Magazinen.
2fa.enable: Zwei-Faktor-Authentifizierung Einrichten
oauth2.grant.entry_comment.edit: Bearbeite deine existierenden Kommentare in
Themen.
2fa.user_active_tfa.title: Nutzer hat aktive 2FA
oauth2.grant.moderate.magazine_admin.update: Bearbeite Regeln, Beschreibungen,
NSFW Status und Icons deiner eigenen Magazine.
oauth2.grant.moderate.entry.trash: Lösche oder stelle Themen in deinen
moderierten Magazinen wieder her.
oauth2.grant.user.oauth_clients.all: Lese und Bearbeite die Berechtigungen die
du anderen OAuth2 Apps gegeben hast.
2fa.code_invalid: Der Authentisierungscode ist ungültig
oauth2.grant.user.profile.read: Lese dein Profil.
oauth2.grant.admin.oauth_clients.read: Zeige die OAuth2 Clients die auf deiner
Instanz existieren und deren Nutzungsstatistik.
oauth2.grant.write.general: Erstelle oder Beatbeite jedes deiner Themen,
Beiträge oder Kommentare.
single_settings: Einzeln
oauth2.grant.moderate.post_comment.set_adult: Kennzeichne Kommentare unter
Beiträgen in deinen moderierten Magazinen als NSFW.
oauth2.grant.moderate.post_comment.change_language: Ändere die Sprache von
Kommentaren unter Beiträgen in deinen moderierten Magazinen.
moderation.report.ban_user_title: Nutzer Sperren
2fa.available_apps: Nutze eine Zwei-Faktor-Authentifizierung-App wie
%google_authenticator%, %aegis% (Android) oder %raivo% (iOS) um den QR-Code zu
scannen.
oauth2.grant.admin.user.verify: Verifiziere Nutzer auf deiner Instanz.
cancel: Abbrechen
oauth2.grant.entry.delete: Lösche deine existierenden Themen.
oauth2.grant.admin.instance.information.edit: Aktualisiere die Über, FAQ,
Kontakt, Nutzungsbedingungen und Datenschutzerklärung Seiten deiner Instanz.
oauth2.grant.read.general: Lese alle Inhalte zu denen du Zugang hast.
oauth2.grant.domain.all: Abonniere oder Blockiere Domains und zeige die Domains
die du abonniert oder blockiert hast.
purge_content_desc: Lösche den Inhalt des Nutzers endgültig, inklusive aller
Antworten anderer Nutzer in den erstellten Themen, Beiträgen und Kommentaren.
flash_post_unpin_success: Der Beitrag wurde erfolgreich gelöst.
oauth2.grant.post_comment.delete: Lösche deine existierenden Kommentare unter
Beiträgen.
oauth2.grant.admin.oauth_clients.revoke: Widerrufe Zugang zu OAuth2 Clients auf
deiner Instanz.
oauth2.grant.admin.instance.settings.edit: Aktualisiere die Einstellungen deiner
Instanz.
oauth2.grant.moderate.entry.set_adult: Kennzeichne Themen als NSFW in deinen
moderierten Magazinen.
oauth2.grant.delete.general: Lösche jedes deiner Themen, Beiträge oder
Kommentare.
oauth2.grant.entry_comment.vote: Favorisiere, booste oder reduziere jeden
Kommentar in einem Thema.
oauth2.grant.admin.instance.stats: Zeige die Statistiken deiner Instanz.
oauth2.grant.admin.instance.settings.all: Zeige oder aktualisiere die
Einstellungen deiner Instanz.
oauth2.grant.entry.all: Erstelle, Bearbeite oder Lösche deine Themen und stimme
ab, booste oder melde jedes Thema.
delete_content: Lösche Inhalt
two_factor_backup: Zwei-Faktor-Authentifizierung Backup Codes
magazine_theme_appearance_background_image: Eigenes Hintergrundbild das
angezeigt wird wenn Inhalte innerhalb deines Magazins angezeigt werden.
oauth2.grant.admin.federation.read: Zeige die deföderierten Instanzen.
oauth2.grant.moderate.entry_comment.change_language: Ändere die Sprache von
Kommentaren in Themen deiner moderierten Magazine.
oauth2.grant.moderate.magazine_admin.stats: Zeige den Inhalt, Abstimmung, und
Ansichtstatus deiner eigenen Magazine.
password_and_2fa: Passwort & 2FA
oauth2.grant.user.message.create: Sende Nachrichten an andere Nutzer.
oauth2.grant.admin.oauth_clients.all: Zeige oder widerrufe OAuth2 Clients die
auf deiner Instanz existieren.
2fa.backup-create.label: Erstelle neue Backup Codes
2fa.backup: Deine Zwei-Faktor Backup Codes
oauth2.grant.post_comment.all: Erstelle, Bearbeite oder Lösche deine Kommentare
zu Beiträgen und favorisiere, booste oder melde jeden Kommentar unter einem
Beitrag.
oauth2.grant.admin.user.purge: Lösche Nutzer endgültig von deiner Instanz.
update_comment: Aktualisiere Kommentar
2fa.qr_code_link.title: Diesen Link aufzurufen kann deiner Plattform erlauben
diese Zwei-Faktor-Authentifizierung zu registrieren
2fa.backup-create.help: Du Kannst neue Backup Codes erstellen; diese Aktion wird
existierende Codes ungültig machen.
oauth2.grant.user.notification.delete: Lösche deine Benachrichtigungen.
two_factor_authentication: Zwei-Faktor-Authentifizierung
show_avatars_on_comments_help: Zeige/Verstecke Nutzeravatare wenn Kommentare in
einem einzelnen Thema oder Beitrag angeschaut werden.
2fa.verify_authentication_code.label: Gebe einen Zwei-Faktor Code ein um die
Einrichtung zu verifizieren
oauth2.grant.post.all: Erstelle, Bearbeite oder Lösche Beiträge und stimme ab,
booste oder melde jeden Beitrag.
oauth2.grant.block.general: Blockiere oder Entblockiere jedes Magazin, Domain,
oder Nutzer und zeige die Magazine, Domains und Nutzer die du blockiert hast.
moderation.report.reject_report_confirmation: Bist du dir sicher dass du diese
Meldung Ablehnen willst?
oauth2.grant.post_comment.create: Erstelle neue Kommentare unter Beiträgen.
oauth2.grant.user.block: Sperre oder entsperre Nutzer und zeige die Nutzer die
blockiert hast.
oauth2.grant.post.delete: Lösche deine existierenden Beiträge.
oauth2.grant.subscribe.general: Abonniere oder Folge jedem Magazin, Domain, oder
Nutzer und zeige die Magazine, Domains und Nutzer an die du abonniert hast.
oauth2.grant.moderate.post_comment.trash: Lösche oder stelle Kommentare unter
Beiträgen in deinen moderierten Magazinen wieder her.
oauth2.grant.moderate.magazine_admin.moderators: Hinzufügen oder Entfernen von
Moderatoren in deinen eigenen Magazinen.
oauth2.grant.admin.entry_comment.purge: Lösche jeden Kommentar in einem Thema
endgültig von deiner Instanz.
comment_reply_position: Kommentarantwortposition
oauth2.grant.post.create: Erstelle neue Beiträge.
oauth2.grant.domain.block: Blockiere oder Entblockiere Domains und zeige die
Domains die du blockiert hast.
oauth2.grant.admin.instance.all: Zeige und aktualisiere Instanzeinstellungen und
-informationen.
oauth2.grant.user.profile.edit: Bearbeite dein Profil.
oauth2.grant.admin.post_comment.purge: Lösche jeden Kommentar unter einem
Beitrag endgültig von deiner Instanz.
oauth2.grant.moderate.magazine_admin.badges: Erstelle oder Entferne Abzeichen in
deinen eigenen Magazinen.
edit_my_profile: Mein Profil bearbeiten
user_badge_moderator: Mod
flash_account_settings_changed: Deine Konto Einstellungen wurden erfolgreich
geändert. Du musst dich erneut anmelden.
close: Schließen
restore_magazine: Magazin wiederherstellen
flash_post_new_success: Beitrag wurde erfolgreich erstellt.
flash_comment_edit_error: Kommentar konnte nicht bearbeitet werden. Etwas ist
schief gegangen.
flash_comment_new_error: Kommentar konnte nicht erstellt werden. Etwas ist
schief gegangen.
account_settings_changed: Die Kontoeinstellungen wurden erfolgreich geändert. Du
musst dich erneut anmelden.
suspend_account: Konto suspendieren
flash_post_new_error: Beitrag konnte nicht erstellt werden. Etwas ist schief
gegangen.
default_theme: Standard Design
page_width_fixed: Festgelegt
flash_thread_new_error: Thema konnte nicht erstellt werden. Etwas ist schief
gelaufen.
remove_following: Folgen entfernen
account_suspended: Das Konto wurde suspendiert.
mark_as_adult: Als NSFW markieren
user_badge_admin: Admin
flash_magazine_theme_changed_error: Die Magazin Darstellung konnte nicht
aktualisiert werden.
flash_post_edit_error: Beitrag konnte nicht bearbeitet werden. Etwas ist schief
gegangen.
2fa.setup_error: Fehler bei der 2FA Aktivierung für das Konto
page_width_auto: Automatisch
flash_post_edit_success: Beitrag erfolgreich bearbeitet.
flash_user_edit_password_error: Passwort konnte nicht geändert werden.
user_badge_global_moderator: Globaler Mod
apply_for_moderator: Als Moderator bewerben
unmark_as_adult: NSFW Markierung entfernen
position_bottom: Unten
deletion: Löschung
deleted_by_author: Thema, Beitrag oder Kommentar wurde durch den Author gelöscht
account_unbanned: Dieses Konto wurde entsperrt.
solarized_auto: Sonnig (Auto Erkennung)
flash_magazine_theme_changed_success: Die Magazin Darstellung wurde erfolgreich
aktualisiert.
change_my_cover: Mein Banner ändern
flash_thread_edit_error: Thema konnte nicht bearbeitet werden. Etwas ist schief
gegangen.
announcement: Ankündigung
flash_user_edit_email_error: E-Mail konnte nicht geändert werden.
page_width_max: Maximal
sensitive_toggle: Sichtbarkeit für sensible Inhalte umschalten
magazine_is_deleted: Magazin ist gelöscht. Du kannst es innerhalb von 30 Tagen
wiederherstellen .
sensitive_show: Klicke zum anzeigen
open_url_to_fediverse: Original-URL öffnen
page_width: Seiten Breite
user_badge_bot: Bot
flash_comment_new_success: Kommentar wurde erfolgreich erstellt.
flash_user_settings_general_error: Nutzer-Einstellungen konnten nicht
gespeichert werden.
account_is_suspended: Dieses Nutzerkonto ist suspendiert.
user_suspend_desc: Dein Konto suspendieren versteckt deine Inhalte auf der
Instanz, löscht diese aber nicht permanent, du kannst die Inhalte jederzeit
wiederherstellen.
ownership_requests: Eigentümer Anfragen
flash_email_was_sent: E-Mail wurde erfolgreich versandt.
account_unsuspended: Die Kontosuspendierung wurde aufgehoben.
sensitive_warning: Sensibler Inhalt
keywords: Schlüsselwörter
moderator_requests: Moderator Anfragen
default_theme_auto: Hell/Dunkel (Auto Erkennung)
flash_comment_edit_success: Kommentar wurde erfolgreich aktualisiert.
action: Aktion
flash_mark_as_adult_success: Die NSFW Markierung wurde dem Post erfolgreich
hinzugefügt.
cancel_request: Anfrage abbrechen
unsuspend_account: Konto Suspendierung aufheben
deleted_by_moderator: Thema, Beitrag oder Kommentar wurde durch den Moderator
gelöscht
flash_unmark_as_adult_success: Die NSFW Markierung wurde vom Post erfolgreich
entfernt.
abandoned: Verlassen
account_banned: Dieses Konto wurde gesperrt.
change_my_avatar: Meinen Avatar ändern
flash_user_edit_profile_success: Profil-Einstellungen erfolgreich gespeichert.
flash_user_settings_general_success: Nutzer Einstellungen erfolgreich
gespeichert.
remove_subscriptions: Abonnements entfernen
delete_magazine: Magazin löschen
sensitive_hide: Klicke zum verstecken
request_magazine_ownership: Magazin Eigentümerschaft beantragen
pending: Unerledigt
accept: Akzeptieren
flash_email_failed_to_sent: E-Mail konnte nicht gesendet werden.
flash_user_edit_profile_error: Profil-Einstellungen konnten nicht gespeichert
werden.
user_badge_op: OP
purge_magazine: Magazin permanent löschen
magazine_deletion: Magazin Löschung
position_top: Oben
subscribers_count: '{0}Abonnenten|{1}Abonennt|]1,Inf[ Abonnenten'
followers_count: '{0}Follower|{1}Follower|]1,Inf[ Follower'
remove_media: Medien entfernen
menu: Menü
all_time: Gesamt
details: Details
spoiler: Spoiler
sso_registrations_enabled: SSO Registrierung aktiviert
marked_for_deletion: für Löschung markiert
marked_for_deletion_at: für Löschung am %date% markiert
subscribe_for_updates: Abonniere um Updates zu erhalten.
account_deletion_title: Kontolöschung
account_deletion_description: Dein Konto wird in 30 Tagen gelöscht außer du
entscheidest dich dein Konto sofort zu löschen. Um das Konto
wiederherzustellen, logge dich in den nächsten 30 Tagen mit deinen
Zugangsdaten ein oder kontaktiere einen Administrator.
account_deletion_button: Lösche Konto
account_deletion_immediate: Sofort Löschen
remove_schedule_delete_account: Entferne geplante Löschung
remove_schedule_delete_account_desc: Entfernt die geplante Löschung. Aller
Inhalt wird wieder verfügbar sein und der Nutzer wird sich wieder anmelden
können.
disconnected_magazine_info: Dieses Magazin erhält keine Updates (letzte
Aktivität vor %days% Tage(n)).
always_disconnected_magazine_info: Dieses Magazin erhält keine Updates.
schedule_delete_account: Plane Löschung
schedule_delete_account_desc: Plane die Löschung des Kontos in 30 Tagen. Dies
wird den Nutzer und deren Inhalt verstecken und die Anmeldung verhindern.
sso_registrations_enabled.error: Neue Konto Registrierung mit Identitätsmanager
von Drittanbietern sind momentan deaktiviert.
show: Zeige
hide: Verstecke
edited: Bearbeitet
filter_by_federation: 'Filtern nach Föderierungsstatus'
auto: 'Automatisch'
filter_labels: 'Filter Beschreibung'
restrict_magazine_creation: 'Erstellung lokaler Magazine auf Admins und globale Mods
beschränken'
sort_by: 'Sortieren nach'
filter_by_subscription: 'Filtern nach Abonnements'
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: von
someone: Jemand
cake_day: Kuchen-Tag
last_updated: Letzte Aktualisierung
magazine_log_entry_unpinned: hat Eintrag gelöst
and: und
back: Zurück
magazine_log_entry_pinned: hat Eintrag angeheftet
direct_message: Direktnachricht
manually_approves_followers: Genehmigt Follower manuell
register_push_notifications_button: Push-Benachrichigungen aktivieren
unregister_push_notifications_button: Push-Benachrichigungen deaktivieren
test_push_notifications_button: Push-Benachrichtigung Testen
test_push_message: Hallo Welt!
notification_title_new_comment: Neuer Kommentar
notification_title_removed_comment: Ein Kommentar wurde entfernt
notification_title_edited_comment: Ein Kommentar wurde bearbeitet
notification_title_mention: Du wurdest erwähnt
notification_title_new_reply: Neue Antwort
notification_title_new_thread: Neues Thema
notification_title_removed_thread: Ein Thema wurde entfernt
notification_title_edited_thread: Ein Thema wurde bearbeitet
notification_title_ban: Du wurdest gesperrt
notification_title_message: Neue Direktnachricht
notification_title_new_post: Neuer Beitrag
notification_title_removed_post: Ein Beitrag wurde entfernt
notification_title_edited_post: Ein Beitrag wurde bearbeitet
reported_user: Gemeldeter Nutzer
reporting_user: Meldender Nutzer
report_subject: Inhalt
own_report_rejected: Deine Meldung wurde abgelehnt
open_report: Meldung öffnen
reported: gemeldet
own_report_accepted: Deine Meldung wurde angenommen
own_content_reported_accepted: Eine Meldung deines Inhalts wurde angenommen.
report_accepted: Eine Meldung wurde angenommen
show_active_users: Zeige aktive Nutzer
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 und der letzte erfolgreiche Empfang 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 Moderatoren eingeschränkt und du bist keiner
last_successful_deliver: Letzte erfolgreiche Zustellung
last_successful_receive: Letzter erfolgreicher Empfang
last_failed_contact: Letzter misslungener Kontakt
magazine_posting_restricted_to_mods: Erstellung von Themen auf Moderatoren
beschränken
new_user_description: Dieser Nutzer ist neu (seit weniger als %days% Tagen
aktiv)
new_magazine_description: Dieses Magazin ist neu (seit weniger als %days% Tagen
aktiv)
edit_entry: Thema bearbeiten
flash_image_download_too_large_error: 'Bild konnte nicht erstellt werden, da es zu
groß ist (maximale Größe: %bytes%)'
admin_users_active: Aktiv
admin_users_inactive: Inaktiv
admin_users_suspended: Suspendiert
admin_users_banned: Gesperrt
user_verify: Konto aktivieren
max_image_size: Maximale Dateigröße
change_downvotes_mode: Reduzieren Modus ändern
disabled: Deaktiviert
downvotes_mode: Reduzieren Modus
hidden: Versteckt
enabled: Aktiviert
toolbar.spoiler: Spoiler
comment_not_found: Kommentar nicht gefunden
table_of_contents: Inhaltsverzeichnis
notification_body_new_signup: Der Nutzer %u% hat sich registriert.
notify_on_user_signup: Registrierungen
notification_title_new_signup: Ein Nutzer hat sich registriert
your_account_is_not_yet_approved: Dein Konto wurde noch nicht freigegeben. Wir
werden dir eine E-Mail schicken, sobald die Administratoren deine Anfrage
bearbeitet haben.
bookmark_list_edit: Bearbeiten
bookmarks: Lesezeichen
bookmarks_list: Lesezeichen in %list%
count: Anzahl
bookmark_list_make_default: Zum Standard machen
bookmark_list_create_placeholder: Namen eingeben...
bookmark_list_create_label: Listenname
select_user: Wähle einen Nutzer
signup_requests: Registrierungsanfragen
application_text: Erkläre warum du beitreten möchtest
email_application_approved_title: Deine Registrierungsanfrage wurde freigegeben
signup_requests_header: Registrierungsanfragen
signup_requests_paragraph: Diese Nutzer möchten deinem Server beitreten. Sie
können sich nicht anmelden bevor du die Anfragen freigegeben hast.
bookmark_list_is_default: Ist Standardliste
bookmark_list_selected_list: Liste auswählen
new_users_need_approval: Neue Nutzer müssen von einem Admin freigegeben werden
bevor sie sich anmelden können.
bookmark_add_to_default_list: Lesezeichen zur Standardliste hinzufügen
bookmark_lists: Lesezeichenlisten
flash_application_info: Ein Admin muss deinen Account freigeben bevor du dich
anmelden kannst. Wir werden dir eine E-Mail schicken, sobald deine Anfrage
bearbeitet wurde.
is_default: Ist Standard
bookmark_list_create: Erstellen
bookmarks_list_edit: Lesezeichenliste bearbeiten
email_application_rejected_body: Vielen Dank für dein Interesse, aber wir müssen
dir leider mitteilen, dass deine Registrierungsanfrage abgelehnt wurde.
email_application_approved_body: 'Deine Registrierungsanfrage wurde von den Server-Administratoren
freigegeben. Du kannst dich jetzt unter %siteName% auf dem
Server anmelden.'
email_application_rejected_title: Deine Registrierungsanfrage wurde abgelehnt
email_application_pending: Dein Konto muss von einem Admin freigegeben werden,
bevor du dich anmelden kannst.
email_verification_pending: Du musst deine E-Mail-Adresse bestätigen, bevor du
dich anmelden kannst.
bookmark_add_to_list: Lesezeichen zu %list% hinzufügen
search_type_entry: Themen
notification_body2_new_signup_approval: Du musst die Anfrage freigeben bevor
sich der Nutzer anmelden kann
bookmark_remove_all: All Lesezeichen entfernen
search_type_post: Beiträge
bookmark_remove_from_list: Lesezeichen von %list% entfernen
search_type_all: Alles
viewing_one_signup_request: Du schaust dir eine einzelne Registrierungsanfrage
von %username% an
show_magazine_domains: Zeige Magazin-Domains
oauth2.grant.user.bookmark: Hinzufügen und Löschen von Lesezeichen
oauth2.grant.user.bookmark.add: Lesezeichen hinzufügen
oauth2.grant.user.bookmark.remove: Lesezeichen löschen
oauth2.grant.user.bookmark_list: Deine Lesezeichenlisten lesen, bearbeiten und
löschen
oauth2.grant.user.bookmark_list.read: Deine Lesezeichenlisten lesen
oauth2.grant.user.bookmark_list.edit: Deine Lesezeichenlisten bearbeiten
oauth2.grant.user.bookmark_list.delete: Deine Lesezeichenlisten löschen
show_user_domains: Zeige Nutzer-Domains
answered: antwortete
by: von
front_default_sort: Standardsortierung Startseite
comment_default_sort: Standardsortierung Kommentare
open_signup_request: Registrierungsanfrage öffnen
image_lightbox_in_list: Themen-Vorschaubild öffnet Vollbild
compact_view_help: Eine kompakte Ansicht mit geringeren Abständen, bei der die
Bilder auf der rechten Seite angezeigt werden.
remove_user_avatar: Avatar entfernen
image_lightbox_in_list_help: Wenn aktiviert werden Bilder in einem Pop-Up Dialog
vergrößert dargestellt nachdem man auf ein Vorschaubild geklickt hat.
Andernfalls wird das Thema geöffnet.
remove_user_cover: Banner entfernen
show_thumbnails_help: Zeige Vorschaubilder.
show_new_icons: Zeige das Neu-Icon
show_new_icons_help: Zeige ein Icon neben neuen Magazinen/Nutzern an (30 Tage
alt oder jünger)
show_users_avatars_help: Zeige Avatare von Nutzern.
show_magazines_icons_help: Zeige Icons von Magazinen.
toolbar.emoji: Emoji
2fa.manual_code_hint: Falls du den QR-Code nicht einlesen kannst, gib den
Schlüssel manuell ein
magazine_instance_defederated_info: Die Instanz dieses Magazins ist deföderiert.
Das Magazine wird deshalb keine Aktualisierungen erhalten.
user_instance_defederated_info: Die Instanz dieses Nutzers ist deföderiert.
flash_thread_instance_banned: Die Instanz dieses Magazines ist gesperrt.
show_rich_mention_help: Zeige eine Nutzer-Komponente an wenn ein Nutzer erwähnt
wird. Dies beinhaltet deren Anzeigename und Avatar.
show_rich_mention_magazine_help: Zeige eine Magazin-Komponente an wenn ein
Magazine erwähnt wird. Dies beinhaltet dessen Anzeigename und Icon.
show_rich_ap_link_help: Zeige eine Komponente wenn anderer ActivityPub-Inhalt
verlinkt wird.
attitude: Einstellung
show_rich_mention: Schöne Erwähnungen
show_rich_mention_magazine: Schöne Magazin-Erwähnungen
show_rich_ap_link: Schöne AP Links
type_search_term_url_handle: Suchbegriff, URL oder Nutzername eingeben
search_type_magazine: Magazine
search_type_user: Nutzer
search_type_actors: Magazine + Nutzer
search_type_content: Themen + Mikroblogs
type_search_magazine: Suche auf Magazin eingrenzen...
type_search_user: Suche auf Author eingrenzen...
modlog_type_entry_deleted: Thema gelöscht
modlog_type_entry_restored: Thema wiederhergestellt
modlog_type_entry_comment_deleted: Themen-Kommentar gelöscht
modlog_type_entry_comment_restored: Themen-Kommentar wiederhergestellt
modlog_type_entry_pinned: Thema angeheftet
modlog_type_entry_unpinned: Thema gelöst
modlog_type_post_deleted: Mikroblog gelöscht
modlog_type_post_restored: Mikroblog wiederhergestellt
modlog_type_post_comment_deleted: Mikroblog-Kommentar gelöscht
modlog_type_post_comment_restored: Mikroblog-Kommentar wiederhergestellt
modlog_type_ban: Nutzer vom Magazine gebannt
modlog_type_moderator_add: Magazin-Moderator hinzugefügt
modlog_type_moderator_remove: Magazin-Moderator entfernt
banner: Banner
magazine_theme_appearance_banner: Nutzerdefiniertes Banner für das Magazin. Es
wird über allen Themen angezeigt und sollte in einem breiten Format sein (5:1
oder 1500px * 300px).
everyone: Jeder
nobody: Niemand
followers_only: Nur Follower
direct_message_setting_label: Wer kann dir eine Direktnachricht senden
delete_magazine_icon: Magazin Icon löschen
flash_magazine_theme_icon_detached_success: Magazin Icon erfolgreich gelöscht
delete_magazine_banner: Magazin Banner löschen
flash_magazine_theme_banner_detached_success: Magazin Banner erfolgreich
gelöscht
crosspost: Cross-Post
flash_thread_ref_image_not_found: Das Bild, welches durch 'imageHash'
referenziert wird, konnte nicht gefunden werden.
federation_uses_allowlist: Nutze eine Erlaubnisliste für Föderation
defederating_instance: Deföderation von Instanz %i
their_user_follows: Anzahl an Nutzern von deren Instanz die Nutzern auf unserer
Instanz folgen
our_user_follows: Anzahl an Nutzern von unserer Instanz die Nutzern auf deren
Instanz folgen
their_magazine_subscriptions: Anzahl an Nutzern von deren Instanz die ein
Magazin auf unserer Instanz abonniert haben
our_magazine_subscriptions: Anzahl an Nutzern von unserer Instanz die ein
Magazin auf deren Instanz abonniert haben
confirm_defederation: Deföderation bestätigen
flash_error_defederation_must_confirm: Du musst die Deföderation bestätigen
allowed_instances: Erlaubte Instanzen
btn_deny: Verweigern
btn_allow: Erlauben
ban_instance: Instanz sperren
allow_instance: Instanz erlauben
federation_page_use_allowlist_help: Wenn eine Erlaubnisliste genutzt wird, wird
diese Instanz nur mit explizit erlaubten Instanzen föderieren. Andernfalls
wird diese Instanz mit allen Instanzen föderieren, mit Ausnahme derer die
gesperrt sind.
ban_expires: Sperre läuft ab
you_have_been_banned_from_magazine: Du wurdest in Magazine %m gesperrt.
you_have_been_banned_from_magazine_permanently: Du wurdest dauerhaft in Magazin
%m gesperrt.
you_are_no_longer_banned_from_magazine: Du ist nicht mehr in Magazin %m
gesperrt.
front_default_content: Startseiten Standard-Ansicht
default_content_default: Server Standard (Themen)
default_content_combined: Themen + Mikroblog
default_content_threads: Themen
default_content_microblog: Mikroblog
combined: Kombiniert
sidebar_sections_random_local_only: '"Zufällige Themen/Posts" in der Seitenleiste
auf lokale Themen beschränken'
sidebar_sections_users_local_only: '"Aktive Nutzer" in der Seitenleiste auf lokale
beschränken'
random_local_only_performance_warning: Aktivierung von "Zufällige nur lokal"
kann zu SQL Performance Problemen führen.
oauth2.grant.moderate.entry.lock: Themen in deinen moderierten Magazinen
sperren, damit niemand darunter Kommentieren kann
oauth2.grant.moderate.post.lock: Mikroblogs in deinen moderierten Magazinen
sperren, damit niemand darunter kommentieren kann
discoverable: Auffindbar
user_discoverable_help: Wenn dies aktiviert ist können dein Profil, deine
Themen, Mikroblogs und Kommentare über die Suche und die zufälligen Panele
gefunden werden. Dein Profil könnte außerdem in dem aktive Nutzer Panel und
auf der Personen Seite angezeigt werden. Wenn dies deaktiviert ist sind deine
Posts nach wie vor sichtbar für andere Nutzer, sie werden aber nicht in dem
Alle Feed angezeigt.
magazine_discoverable_help: Wenn dies aktiviert ist kann dieses Magazin und alle
Themen, Mikroblogs und Kommentar in diesem Magazin über die Suche und die
zufälligen Panele gefunden werden. Wenn dies deaktiviert ist wird dieses
Magazin nach wie vor in der Magazinliste angezeigt, aber Themen und Mikroblogs
werden nicht in dem Alle Feed angezeigt.
flash_thread_lock_success: Thema erfolgreich gesperrt
flash_thread_unlock_success: Thema erfolgreich entsperrt
flash_post_lock_success: Mikroblog erfolgreich gesperrt
flash_post_unlock_success: Mikroblog erfolgreich entsperrt
lock: Sperren
unlock: Entsperren
comments_locked: Die Kommentare sind gesperrt.
magazine_log_entry_locked: hat die Kommentar gesperrt von
magazine_log_entry_unlocked: hat die Kommentare entsperrt von
modlog_type_entry_lock: Thema gesperrt
modlog_type_entry_unlock: Thema entsperrt
modlog_type_post_lock: Mikroblog gesperrt
modlog_type_post_unlock: Mirkoblog entsperrt
contentnotification.muted: Stummschalten | keine Benachrichtigungen erhalten
contentnotification.default: Standard | erhalte Benachrichtigungen anhand deiner
Standard-Einstellungen
contentnotification.loud: Laut | erhalte alle Benachrichtigungen
indexable_by_search_engines: Indizierbar von Suchmaschinen
user_indexable_by_search_engines_help: Wenn diese Einstellung aus ist, werden
Suchmaschinen angewiesen deine Themen und Mikroblogs nicht zu indizieren,
allerdings werden deine Kommentare davon nicht beeinflusst und böswillige
Akteure könnten es ignorieren. Diese Einstellung wird auch an andere Server
föderiert.
magazine_indexable_by_search_engines_help: Wenn diese Einstellung aus ist,
werden Suchmaschinen angewiesen keins der Themen und Mikroblogs dieses
Magazins zu indizieren. Das beinhaltet die Front Seite und alle
Kommentar-Seiten. Diese Einstellung wird auch an andere Server föderiert.
magazine_name_as_tag: Nutze den Magazinnamen als Hashtag
magazine_name_as_tag_help: Die Hashtags eines Magazine werden genutzt um
Mikroblogs einem Magazin zuzuweise. Wenn ein Magazin den Namen "fediverse" hat
und dieses Magazine den Hashtag "fediverse" enthält, wird jeder Mikroblog mit
dem Hashtag "#fediverse" diesem Magazine zugewiesen.
magazine_rules_deprecated: das Regel-Feld ist veraltet und wird in der Zukunft
entfernt werden. Bitte schreib die Regeln in das Beschreibungs-Feld.
created_since: Erstellt seit
displayname: Anzeigename
show_boost_following_label: Zeige geboostete Inhalte in den Mikroblog und
Kombiniert-Feeds
monitoring: Überwachung
monitoring_queries: '{0}SQL Abfragen|{1}SQL Abfrage|]1,Inf[ SQL Abfragen'
monitoring_duration_min: minimum
monitoring_duration_mean: durchschnitt
monitoring_duration_max: maximum
monitoring_query_count: anzahl
monitoring_query_total: gesamt
monitoring_duration: Dauer
monitoring_dont_group_similar: ähnliche Abfragen nicht zusammenfassen
monitoring_group_similar: ähnliche Abfragen zusammenfassen
monitoring_http_method: HTTP Methode
monitoring_url: URL
monitoring_request_successful: Erfolgreich
monitoring_user_type: Nutzer-Typ
monitoring_path: Route/Nachrichten Klasse
monitoring_handler: Controller/Transport
monitoring_started: Gestartet
monitoring_twig_renders: Twig Render
monitoring_curl_requests: Curl Anfragen
monitoring_route_overview: Prozesslaufzeit summiert pro Route
monitoring_route_overview_description: Dieser Graph zeigt die summierte
Prozesslaufzeit in Millisekunden pro Route/Nachricht
monitoring_duration_overall: Andere
monitoring_duration_query: Abfrage
monitoring_duration_twig_render: Twig Render
monitoring_duration_curl_request: Curl Anfrage
monitoring_duration_sending_response: Antwort senden
monitoring_dont_format_query: Abfrage nicht formatieren
monitoring_format_query: Abfrage formatieren
monitoring_dont_show_parameters: Parameter nicht anzeigen
monitoring_show_parameters: Parameter anzeigen
monitoring_execution_type: Verarbeitungstyp
monitoring_request: HTTP Anfrage
monitoring_messenger: Messenger
monitoring_anonymous: Anonym
monitoring_user: Nutzer
monitoring_activity_pub: ActivityPub
monitoring_ajax: AJAX
monitoring_created_from: Gestartet nach
monitoring_created_to: Gestartet vor
monitoring_duration_minimum: Minimale Dauer
monitoring_submit: Filtern
monitoring_has_exception: Fehlerhaft
monitoring_chart_ordering: Diagramm Sortierung
monitoring_total_duration: Gesamtdauer
monitoring_mean_duration: Durchschnittsdauer
monitoring_twig_compare_to_total: Vergleiche mit Gesamtdauer
monitoring_twig_compare_to_parent: Vergleiche mit übergeordneter Dauer
monitoring_disabled: Überwachung ist deaktiviert.
monitoring_queries_enabled_persisted: Abfragenüberwachung ist aktiviert.
monitoring_queries_enabled_not_persisted: Abfragenüberwachung ist aktiviert,
aber nur für die Prozessdauer.
monitoring_queries_disabled: Abfragenüberwachung ist deaktiviert.
monitoring_twig_renders_enabled_persisted: Twig-Render-Überwachung ist
aktiviert.
monitoring_twig_renders_enabled_not_persisted: Twig-Render-Überwachung, aber nur
für die Prozessdauer.
monitoring_twig_renders_disabled: Twig-Render-Überwachung ist deaktiviert.
monitoring_curl_requests_enabled_persisted: Curl-Anfragen-Überwachung ist
aktiviert.
monitoring_curl_requests_enabled_not_persisted: Curl-Anfragen-Überwachung ist
aktiviert, aber nur für die Prozessdauer.
monitoring_curl_requests_disabled: Curl-Anfragen-Überwachung ist deaktiviert.
reached_end: Du hast das Ende erreicht
first_page: Erste Seite
next_page: Nächste Seite
previous_page: Vorherige Seite
filter_list_create: Filter-Liste erstellen
filter_lists: Filterlisten
filter_lists_where_to_filter: Wo soll der Filter angewandt werden
filter_lists_filter_words: Gefilterte Wörter
expiration_date: Ablaufdatum
filter_lists_filter_location: Aktiv in
filter_lists_word_exact_match: Exakte Übereinstimmung
filter_lists_word_exact_match_help: Wenn exakte Übereinstimmung aktiviert ist,
wird die Suche die Groß-/Kleinschreibung beachten
feeds: Feeds
filter_lists_feeds_help: Filtere die Wörter in Themen, Mikroblogs und
Kommentaren in Feeds, wie zum Beispiel /all, /sub, Magazin-Feeds, etc.
filter_lists_comments_help: Filtere die Wörter während du Themen oder Mikroblogs
anschaust in den Kommentaren.
filter_lists_profile_help: Filtere die Wörter während du dir das Profil eines
Nutzers und seine Inhalte ansiehst.
expired: Abgelaufen
================================================
FILE: translations/messages.el.yaml
================================================
type.link: Σύνδεσμος
type.article: Νήμα
type.photo: Φωτογραφία
type.video: Βίντεο
type.magazine: Περιοδικό
thread: Νήμα
threads: Νήματα
microblog: Μικροϊστολόγιο
events: Γεγονότα
magazine: Περιοδικό
magazines: Περιοδικά
search: Αναζήτηση
add: Προσθήκη
select_channel: Επίλεξε ένα κανάλι
login: Σύνδεση
active: Ενεργά
newest: Νεότερα
oldest: Παλαιότερα
commented: Με σχόλια
filter_by_time: Φιλτράρισμα βάσει χρόνου
filter_by_type: φιλτράρισμα βάσει τύπου
favourites: Θετικοί ψήφοι
favourite: Αγαπημένο
avatar: Άβαταρ
added: Προστέθηκε
no_comments: Χωρίς σχόλια
created_at: Δημιουργήθηκε
subscribers: Συνδρομητές
online: Σε σύνδεση
comments: Σχόλια
posts: Αναρτήσεις
replies: Απαντήσεις
moderators: Διαχειριστές
add_post: Προσθήκη ανάρτησης
add_media: Προσθήκη πολυμέσων
markdown_howto: Πώς λειτουργεί ο συντάκτης;
activity: Δραστηριότητα
cover: Εξώφυλλο
related_posts: Σχετικές αναρτήσεις
unsubscribe: Απεγγραφή
subscribe: Εγγραφή
follow: Ακολούθησε
reply: Απάντησε
password: Κωδικός
remember_me: Να με θυμάσαι
top: Κορυφαία
you_cant_login: Ξέχασες τον κωδικό σου;
already_have_account: Έχεις ήδη λογαριασμό;
register: Εγγραφή
reset_password: Επαναφορά κωδικού
show_more: Δείξε περισσότερα
email: Email
repeat_password: Επανάληψη κωδικού
terms: Όροι χρήσης
privacy_policy: Πολιτική απορρήτου
about_instance: Σχετικά
all_magazines: Όλα τα περιοδικά
stats: Στατιστικά
fediverse: Fediverse
add_new_article: Προσθήκη νέου νήματος
add_new_link: Προσθήκη νέου συνδέσμου
add_new_photo: Προσθήκη νέας φωτογραφίας
add_new_post: Προσθήκη νέας ανάρτησης
add_new_video: Προσθήκη νέου βίντεο
contact: Επικοινωνία
rss: RSS
change_theme: Αλλαγή θέματος
useful: Χρήσιμα
help: Βοήθεια
check_email: "'Ελεγξε το email σου"
reset_check_email_desc2: Αν δεν έλαβες το email παρακαλώ κοίτα το φάκελο με τα
ανεπιθύμητα.
try_again: Προσπάθησε ξανά
email_confirm_header: Γεια! Επιβεβαίωσε την διεύθυνση email σου.
email_verify: Επιβεβαίωσε την διεύθυνση email
email_confirm_title: Επιβεβαίωσε την διεύθυνση email.
select_magazine: Επίλεξε ένα περιοδικό
add_new: Προσθήκη νέου
url: URL
title: Τίτλος
tags: Ετικέτες
badges: Σήματα
is_adult: 18+ / NSFW
eng: ENG
oc: OC
image: Εικόνα
name: Όνομα
rules: Κανόνες
followers: Ακόλουθοι
subscriptions: Εγγραφές
cards: Κάρτες
columns: Στήλες
user: Χρήστης
joined: Έγινε μέλος
people_local: Τοπικά
hot: Σε τάση
dont_have_account: Δεν έχεις λογαριασμό;
change_view: Αλλαγή όψης
username: Όνομα χρήστη
more: Περισσότερα
agree_terms: Δέχομαι τους %terms_link_start%Όρους και
Προϋποθέσεις%terms_link_end% και την %policy_link_start%Πολιτική
Απορρήτου%policy_link_end%
owner: Ιδιοκτήτης
create_new_magazine: Δημιουργία νέου περιοδικού
mod_log: Καταγραφές συντονισμού
faq: Συχνές ερωτήσεις (FAQ)
add_comment: Προσθήκη σχολίου
reset_check_email_desc: Αν υπάρχει ήδη λογαριασμός με το email σου, σύντομα θα
λάβεις ένα email που θα περιέχει σύνδεσμο ώστε να επαναφέρεις τον κωδικό σου.
Ο σύνδεσμος θα λήξει σε %expire%.
random_posts: Τυχαίες αναρτήσεις
email_confirm_content: 'Θες να ενεργοποιήσεις τον λογαριασμό σου στο Μbin; Πάτα στον
παρακάτω σύνδεσμο:'
federated_user_info: Αυτό το προφίλ είναι από έναν συνενωμένο διακομιστή και
μπορεί να είναι ελλιπές.
email_confirm_expire: Λάβετε υπόψη ότι ο σύνδεσμος θα λήξει σε μία ώρα.
description: Περιγραφή
people: Άτομα
image_alt: Εναλλακτικό κείμενο εικόνας
domain: Τομέας
overview: Επισκόπηση
related_tags: Σχετικές ετικέτες
go_to_content: Μετάβαση στο περιεχόμενο
go_to_filters: Μετάβαση στα φίλτρα
go_to_search: Μετάβαση στην αναζήτηση
subscribed: Εγγεγραμμένος
all: Όλα
logout: Αποσύνδεση
classic_view: Κλασική προβολή
compact_view: Συμπαγής προβολή
chat_view: Προβολή συνομιλίας
tree_view: Προβολή δέντρου
table_view: Προβολή πίνακα
cards_view: Προβολή καρτών
3h: 3ώ
6h: 6ώ
12h: 12ώ
1w: 1εβδ
1m: 1μ
links: Σύνδεσμοι
articles: Νήματα
photos: Φωτογραφίες
videos: Βίντεο
report: Αναφορά
copy_url: Αντιγραφή Mbin URL
copy_url_to_fediverse: Αντιγραφή πρωτότυπου URL
share_on_fediverse: Κοινοποίηση στο Fediverse
are_you_sure: Σίγουρα;
reason: Αιτιολογία
delete: Διαγραφή
edit_post: Επεξεργασία ανάρτησης
edit_comment: Αποθήκευση αλλαγών
settings: Ρυθμίσεις
profile: Προφίλ
blocked: Μπλοκαρισμένοι
reports: Αναφορές
notifications: Ειδοποιήσεις
messages: Μηνύματα
appearance: Εμφάνιση
hide_adult: Απόκρυψη περιεχομένου NSFW
privacy: Ιδιωτικότητα
show_profile_subscriptions: Εμφάνιση εγγραφών σε περιοδικά
notify_on_new_entry_comment_reply: Απαντήσεις στα σχόλια μου σε οποιοδήποτε νήμα
notify_on_new_post_reply: Απαντήσεις οποιοδήποτε επιπέδου σε αναρτήσεις που
εξουσιοδότησα
notify_on_new_entry: Νέα νήματα (σύνδεσμοι ή άρθρα) σε οποιοδήποτε περιοδικό στο
οποίο έχω εγγραφεί
save: Αποθήκευση
about: Σχετικά
old_email: Τρέχον email
new_email: Νέο email
new_email_repeat: Επιβεβαίωση νέου email
current_password: Τρέχων κωδικός πρόσβασης
new_password: Νέος κωδικός πρόσβασης
change_email: Αλλαγή email
change_password: Αλλαγή κωδικού πρόσβασης
expand: Ανάπτυξη
collapse: Σύμπτυξη
domains: Τομείς
error: Σφάλμα
votes: Ψήφοι
theme: Θέμα
dark: Σκοτεινό
light: Φωτεινό
solarized_light: Solarized Φωτεινό
font_size: Μέγεθος γραμματοσειράς
size: Μέγεθος
boosts: Ενισχύσεις
show_users_avatars: Εμφάνιση άβαταρ των χρηστών
yes: Ναι
no: Όχι
show_thumbnails: Εμφάνιση μικρογραφιών
rounded_edges: Στρογγυλεμένες άκρες
removed_thread_by: έχει αφαιρέσει ένα νήμα από
removed_comment_by: έχει αφαιρέσει ένα σχόλιο από
restored_comment_by: έχει επαναφέρει το σχόλιο από
restored_post_by: έχει επαναφέρει την ανάρτηση από
he_banned: αποκλεισμός
he_unbanned: άρση αποκλεισμού
read_all: Ανάγνωση όλων
show_all: Εμφάνιση όλων
flash_thread_edit_success: Το νήμα επεξεργάστηκε επιτυχώς.
flash_thread_delete_success: Το νήμα διαγράφηκε επιτυχώς.
flash_thread_pin_success: Το νήμα καρφιτσώθηκε επιτυχώς.
flash_thread_unpin_success: Το νήμα ξεκαρφιτσώθηκε επιτυχώς.
flash_magazine_edit_success: Το περιοδικό έχει επεξεργαστεί επιτυχώς.
too_many_requests: Υπέρβαση του ορίου, δοκιμάστε ξανά αργότερα.
set_magazines_bar_desc: πρόσθεσε τα ονόματα των περιοδικών μετά το κόμμα
added_new_thread: Πρόσθεσε νέο νήμα
edited_thread: Επεξεργάστηκε ένα νήμα
mod_remove_your_thread: Ένας συντονιστής αφαίρεσε το νήμα σου
added_new_comment: Πρόσθεσε νέο σχόλιο
edited_comment: Επεξεργάστηκε ένα σχόλιο
mod_deleted_your_comment: Ένας συντονιστής διέγραψε το σχόλιο σου
added_new_post: Πρόσθεσε μια νέα ανάρτηση
mod_remove_your_post: Ένας συντονιστής αφαίρεσε την ανάρτησή σου
added_new_reply: Πρόσθεσε μια νέα απάντηση
wrote_message: Έγραψε ένα μήνυμα
mentioned_you: Σε ανέφερε
comment: Σχόλιο
post: Ανάρτηση
send_message: Αποστολή άμεσου μηνύματος
message: Μήνυμα
infinite_scroll: Άπειρη κύλιση
sticky_navbar: Καρφιτσωμένη μπάρα πλοήγησης
subject_reported: Το περιεχόμενο έχει αναφερθεί.
left: Αριστερά
right: Δεξιά
status: Κατάσταση
on: Ενεργό
off: Ανενεργό
upload_file: Ανέβασμα αρχείου
from_url: Από url
magazine_panel: Πίνακας περιοδικών
reject: Απόρριψη
filters: Φίλτρα
approved: Εγκρίθηκε
rejected: Απορρίφθηκε
add_moderator: Προσθήκη συντονιστή
created: Δημιουργήθηκε
expires: Λήγει
perm: Μόνιμο
expired_at: Έληξε στις
icon: Εικονίδιο
done: Ολοκληρώθηκε
pin: Καρφίτσωμα
unpin: Ξεκαρφίτσωμα
change_language: Αλλαγή γλώσσας
change: Αλλαγή
pinned: Καρφιτσωμένο
preview: Προεπισκόπηση
article: Νήμα
reputation: Φήμη
writing: Γραφή
users: Χρήστες
content: Περιεχόμενο
week: Εβδομάδα
weeks: Εβδομάδες
month: Μήνας
months: Μήνες
year: Έτος
admin_panel: Πίνακας Διαχειριστή
dashboard: Ταμπλό
contact_email: Email επικοινωνίας
pages: Σελίδες
type_search_term: Πληκτρολόγησε τον όρο αναζήτησης
registrations_enabled: Εγγραφή ενεργή
registration_disabled: Εγγραφή ανενεργή
restore: Επαναφορά
add_mentions_posts: Προσθήκη ετικετών αναφοράς σε αναρτήσεις
Password is invalid: Ο κωδικός είναι μη έγκυρος.
Your account has been banned: Ο λογαριασμός σου έχει αποκλειστεί.
firstname: Όνομα
send: Αποστολή
active_users: Ενεργοί χρήστες
random_entries: Τυχαία νήματα
delete_account: Διαγραφή λογαριασμού
purge_account: Εκκαθάριση λογαριασμού
ban_account: Αποκλεισμός λογαριασμού
related_magazines: Σχετικά περιοδικά
sidebar: Πλαϊνή μπάρα
auto_preview: Αυτόματη προεπισκόπηση πολυμέσων
dynamic_lists: Δυναμικές λίστες
captcha_enabled: Το Captcha ενεργοποιήθηκε
return: Επιστροφή
boost: Ενίσχυση
1d: 1η
edit: Επεξεργασία
notify_on_new_entry_reply: Σχόλια οποιουδήποτε επιπέδου σε νήματα που
εξουσιοδότησα
1y: 1χρ
general: Γενικά
solarized_dark: Solarized Σκοτεινό
share: Κοινοποίηση
homepage: Αρχική
notify_on_new_post_comment_reply: Απαντήσεις στα σχόλια μου σε οποιεσδήποτε
αναρτήσεις
featured_magazines: Παρεχόμενα περιοδικά
notify_on_new_posts: Νέες αναρτήσεις σε οποιοδήποτε περιοδικό στο οποίο έχω
εγγραφεί
show_magazines_icons: Εμφάνιση εικονιδίων περιοδικών
restored_thread_by: έχει επαναφέρει ένα νήμα από
new_password_repeat: Επιβεβαίωση νέου κωδικού πρόσβασης
removed_post_by: έχει αφαιρέσει την ανάρτηση από
flash_thread_new_success: Το νήμα δημιουργήθηκε επιτυχημένα και είναι ορατό σε
άλλους χρήστες.
flash_magazine_new_success: Το περιοδικό δημιουργήθηκε επιτυχώς. Τώρα μπορείς να
προσθέσεις νέο περιεχόμενο ή να εξερευνήσεις τον πίνακα διαχείρισης του
περιοδικού.
replied_to_your_comment: Απάντησε στο σχόλιό σου
purge: Eκκαθάριση
show_top_bar: Εμφάνιση μπάρας κορυφής
approve: Αποδοχή
trash: Διαγραμμένα
change_magazine: Αλλαγή περιοδικού
note: Σημείωση
local: Τοπικά
FAQ: Συχνές ερωτήσεις (FAQ)
related_entries: Σχετικά νήματα
header_logo: Λογότυπο κεφαλίδας
set_magazines_bar_empty_desc: εάν το πεδίο είναι κενό, τα ενεργά περιοδικά
εμφανίζονται στη γραμμή.
edited_post: Επεξεργάστηκε μια ανάρτηση
sidebar_position: Θέση πλευρικής γραμμής
add_badge: Προσθήκη σήματος
add_mentions_entries: Προσθήκη ετικετών αναφοράς σε νήματα
unban_account: Άρση αποκλεισμού λογαριασμού
browsing_one_thread: Περιηγήσαι σε μόνο ένα νήμα στη συζήτηση! Όλα τα σχόλια
είναι διαθέσιμα στη σελίδα ανάρτησης.
Your account is not active: Ο λογαριασμός σου δεν είναι ενεργός.
random_magazines: Τυχαία περιοδικά
removed: Αφαιρέθηκε από συντονιστή
deleted: Διαγράφηκε από τον συντάκτη
mod_log_alert: ΠΡΟΕΙΔΟΠΟΙΗΣΗ - Το αρχείο καταγραφής του συντονιστή μπορεί να
περιέχει δυσάρεστο ή ενοχλητικό περιεχόμενο που έχει αφαιρεθεί από τους
συντονιστές. Παρακαλούμε δώσε προσοχή.
type.smart_contract: Έξυπνο συμβόλαιο
up_votes: Ενισχύσεις
enter_your_comment: Εισήγαγε το σχόλιό σου
enter_your_post: Εισήγαγε την ανάρτησή σου
comments_count: '{0}Σχόλια|{1}Σχόλιο|]1,Inf[ Σχόλια'
empty: Κενό
unfollow: Κατάργηση ακολούθησης
down_votes: Μειώσεις
federated_magazine_info: Αυτό το περιοδικό είναι από έναν συνενωμένο διακομιστή
και ενδέχεται να είναι ελλιπές.
go_to_original_instance: Προβολή σε απομακρυσμένη οντότητα
login_or_email: Σύνδεση ή email
in: στο
up_vote: Ενίσχυση
down_vote: Μείωση
moderated: Συντονισμένα
reputation_points: Πόντοι φήμης
body: Κείμενο
following: Ακολουθεί
people_federated: Federated
moderate: Συντόνισε
show_profile_followings: Εμφάνιση χρηστών που ακολουθούνται
flash_register_success: Καλώς ήρθες, ο λογαριασμός σου είναι πλέον
εγγεγραμμένος. Ένα τελευταίο βήμα! - Στα εισερχόμενά σου θα βρεις έναν
σύνδεσμο ενεργοποίησης ώστε να δώσεις ζωή στον λογαριασμό σου.
set_magazines_bar: Μπάρα περιοδικών
banned: Σε απέκλεισε
ban_expired: Ο αποκλεισμός έληξε
ban: Αποκλεισμός
bans: Αποκλεισμοί
add_ban: Προσθήκη αποκλεισμού
federation: Ομοσπονδία
instances: Οντότητες
meta: Meta
federated: Ομοσπονδιακός
instance: Οντότητα
federation_enabled: Ενεργοποιήθηκε η ομοσπονδία
magazine_panel_tags_info: Παροχή μόνο εάν θες περιεχόμενο από το fediverse να
περιλαμβάνεται σε αυτό το περιοδικό με βάση ετικέτες
banned_instances: Οντότητες σε αποκλεισμό
kbin_intro_title: Εξερεύνησε το Fediverse
kbin_intro_desc: είναι μια αποκεντρωμένη πλατφόρμα για συγκέντρωση περιεχομένου
και μικροϊστολόγια που λειτουργεί εντός του δικτύου Fediverse.
kbin_promo_title: Δημιούργησε τη δική σου οντότητα
kbin_promo_desc: '%link_start%Κλωνοποίηση αποθετηρίου%link_end% και ανάπτυξη fediverse'
to: σε
report_issue: Αναφορά προβλήματος
tokyo_night: Τόκυο Νύχτα
mercure_enabled: Το Mercure είναι ενεργό
preferred_languages: Φιλτράρισμα γλωσσών των νημάτων και των αναρτήσεων
infinite_scroll_help: Φόρτωσε αυτόματα περισσότερο περιεχόμενο όταν φτάσεις το
τέλος της σελίδας.
sticky_navbar_help: Η γραμμή πλοήγησης θα καρφιτσωθεί στην κορυφή της σελίδας
όταν κάνεις κύλιση προς τα κάτω.
auto_preview_help: Εμφάνιση των προεπισκοπήσεων πολυμέσων (φωτό, βίντεο) σε
μεγαλύτερο μέγεθος κάτω απ' το περιεχόμενο.
reload_to_apply: Ανανέωση σελίδας για να εφαρμοστούν οι αλλαγές
filter.fields.label: Επιλογή πεδίων για αναζήτηση
filter.fields.only_names: Μόνο ονόματα
filter.fields.names_and_descriptions: Ονόματα και περιγραφές
filter.adult.hide: Απόκρυψη NSFW
filter.adult.show: Εμφάνιση NSFW
filter.adult.label: Επιλογή εμφάνισης περιεχομένου NSFW
filter.adult.only: Μόνο NSFW
toolbar.bold: Έντονα
your_account_is_not_active: Ο λογαριασμός σου δεν έχει ενεργοποιηθεί. Παρακαλώ
έλεγξε το email σου για οδηγίες περί ενεργοποίησης λογαριασμού ή αιτήσου ένα νέο email ενεργοποιήσης λογαριασμού.
your_account_has_been_banned: Ο λογαριασμός σου έχει αποκλειστεί
filter.origin.label: Επιλογή προέλευσης
kbin_bot: Πράκτορας Mbin
toolbar.header: Κεφαλίδα
toolbar.quote: Παράθεση
toolbar.code: Κώδικας
toolbar.link: Σύνδεσμος
toolbar.image: Εικόνα
toolbar.mention: Αναφορά
password_confirm_header: Επιβεβαίωσε το αίτημα αλλαγής κωδικού πρόσβασης.
filter_by_subscription: Φιλτράρισμα βάσει εγγραφής
filter_by_federation: Φιλτράρισμα βάσει κατάστασης ομοσπονδίας
sort_by: Ταξινόμηση βάσει
subscribers_count: '{0}Εγγεγραμμένοι|{1}Εγγεγραμμένος|]1,Inf[ Εγγεγραμμένοι'
followers_count: '{0}Ακόλουθοι|{1}Ακόλουθος|]1,Inf[ Ακόλουθοι'
marked_for_deletion: Επισημάνθηκε για διαγραφή
marked_for_deletion_at: Επισημάνθηκε για διαγραφή στις %date%
remove_media: Αφαίρεση πολυμέσων
edit_entry: Επεξεργασία νήματος
default_theme_auto: Ανοιχτό/Σκοτεινό (Αυτόματος Εντοπισμός)
unban: Άρση αποκλεισμού
ban_hashtag_btn: Αποκλεισμός Ετικέτας
ban_hashtag_description: Αποκλείοντας μία ετικέτα θα αποτρέπει τη δημιουργία
αναρτήσεων μ' αυτή την ετικέτα, αλλά και θα αποκρύπτει υπάρχουσες μ' αυτή την
ετικέτα.
unban_hashtag_description: Αφαιρόντας τον αποκλεισμό μιας ετικέτας θα
επιτρέπεται η δημιουργία αναρτήσεων μ' αυτή την ετικέτα ξανά. Υπάρχουσες
αναρτήσεις μ' αυτή δεν θα κρύβονται.
mark_as_adult: Επισήμανση ως NSFW
unmark_as_adult: Άρση επισήμανσης ως NSFW
tag: Ετικέτα
from: από
default_theme: Προεπιλεγμένο θέμα
solarized_auto: Solarized (Αυτόματος Εντοπισμός)
local_and_federated: Τοπική και σε ομοσπονδία
menu: Μενού
flash_mark_as_adult_success: Αυτή η ανάρτηση έχει επισημανθεί επιτυχώς ως NSFW.
flash_unmark_as_adult_success: Αυτή η ανάρτηση δεν είναι πλέον επισημασμένη ως
NSFW.
disconnected_magazine_info: Αυτό το περιοδικό δεν λαμβάνει ενημερώσεις
(τελευταία δραστηριότητα %days% ημέρα/-ες πριν).
always_disconnected_magazine_info: Αυτό το περιοδικό δεν λαμβάνει ενημερώσεις.
subscribe_for_updates: Κάνε εγγραφή για να λαμβάνεις ενημερώσεις.
unban_hashtag_btn: Άρση αποκλεισμού Ετικέτας
resend_account_activation_email_question: Ανενεργός λογαριασμός;
federated_search_only_loggedin: Η αναζήτηση σε οποσπονδία είναι περιορισμένη αν
δεν έχεις συνδεθεί
oauth.consent.to_allow_access: Για να επιτρεπεί αυτή η πρόσβαση, κάνε κλικ στο
κουμπί "Επιτρέπεται" παρακάτω
toolbar.unordered_list: Μη τακτοποιημένη λίστα
federation_page_allowed_description: Γνωστές οντότητες με τις οποίες έχουμε
συνενωθεί
federation_page_disallowed_description: Οντότητες με τις οποίες δε συνενωνόμαστε
account_deletion_title: Διαγραφή λογαριασμού
account_deletion_button: Διαγραφή Λογαριασμού
errors.server404.title: 404 Δε βρέθηκε
errors.server500.title: 500 Εσωτερικό Σφάλμα Διακομιστή
email_confirm_link_help: Εναλλακτικά μπορείς να κάνεις αντιγραφή επικόλληση στον
περιηγητή σου το ακόλουθο
bot_body_content: "Καλώς ήρθατε στον Πράκτορα του Mbin! Αυτός ο πράπτορας έχει καθοριστικό
ρόλο στην διατήρηση της λειτουργικότητας του ActivityPub εντός του Mbin. Εξασφαλίζει
ότι το Mbin μπορεί να επικοινωνεί και να συνενώνεται με άλλες οντότητες στο fediverse.\n\
\ \nΤο ActivityPub είναι ένα ανοιχτό πρωτόκολλο που επιτρέπει στις αποκεντρωμένες
πλατφόρμες κοινωνικής δικτύωσης να επικοινωνούν και να αλληλεπιδρούν μεταξύ τους.
Επιτρέπει στους χρήστες σε διαφορετικές οντότητες (διακομιστές) να ακολουθούν, να
αλληλεπιδρούν και να μοιράζονται περιεχόμενο μέσω του ομοσπονδιακού κοινωνικού δικτύου
που είναι γνωστό ως fediverse. Παρέχει έναν τυποποιημένο τρόπο στους χρήστες να
δημοσιεύουν περιεχόμενο, να ακολουθούν άλλους χρήστες και να συμμετέχουν σε κοινωνικές
αλληλεπιδράσεις όπως να τους αρέσει, να μοιράζονται και να σχολιάζουν νήματα ή αναρτήσεις."
toolbar.strikethrough: Γραμμή διαγραφής
toolbar.ordered_list: Τακτοποιημένη Λίστα
federation_page_enabled: Σελίδα ομοσπονδίας ενεργή
resend_account_activation_email_error: Υπήρξε ένα ζήτημα κατοχύρωσης αυτού του
αιτήματος. Μπορεί να μην υπάρχει λογαριασμός που να σχετίζεται μ' αυτό το
email ή ίσως έχει ήδη ενεργοποιηθεί.
block: Αποκλεισμός
toolbar.italic: Πλάγια
federation_page_dead_title: Νεκρές οντότητες
errors.server429.title: 429 Υπερβολικά Πολλές Αιτήσεις
account_deletion_description: Ο λογαριασμός σου θα διαγραφεί σε 30 ημέρες εκτός
αν επιλέξεις να τον διαγράψεις αμέσως. Για να τον επαναφέρεις εντός 30 ημέρων,
συνδέσου με τα ίδια στοιχεία σύνδεσης χρήστη ή επικοινώνησε με έναν
διαχειριστή.
account_deletion_immediate: Άμεση διαγραφή
more_from_domain: Περισσότερα απ' τον τομέα
oauth.consent.app_requesting_permissions: θα 'θελε να πραγματοποιήσει τις
ακόλουθες ενέργειες εκ μέρους σου
oauth.consent.allow: Επιτρέπεται
resend_account_activation_email_description: Εισήγαγε τη διεύθυνση email που
σχετίζεται με τον λογαριασμό σου. Θα σου στείλουμε άλλο ένα email
ενεργοποίησης.
oauth.consent.app_has_permissions: μπορεί ήδη να πραγματοποιήσει τις ακόλουθες
ενέργειες
custom_css: Προσαρμοσμένη CSS
federation_page_dead_description: Οντότητες στις οποίες δε μπορέσαμε να
παραδώσουμε 10 δραστηριότητες στη σειρά και όπου η τελευταία παράδοση και
παραλαβή ήταν μια εβδομάδα πριν
errors.server500.description: Συγγνώμη, κάτι πήγε στραβά από τη πλευρά μας. Αν
συνεχίζεις να βλέπεις αυτό το σφάλμα, δοκίμασε να επικοινωνήσεις με τον
ιδιοκτήτη της οντότητας. Αν η οντότητα δε δουλεύει καθόλου, ρίξε μια ματιά
εντωμεταξύ, σε %link_start%άλλες οντότητες του Mbin%link_end% μέχρι να
επιλυθεί το πρόβλημα.
errors.server403.title: 403 Απαγορευμένο
email_confirm_button_text: Επιβεβαίωσε την αίτηση για αλλαγή κωδικού
email.delete.title: Αίτημα διαγραφής λογαριασμού χρήστη
email.delete.description: Ο ακόλουθος χρήστης αιτήθηκε διαγραφής του λογαριασμού
του
resend_account_activation_email: Επαναποστολή email ενεργοποίησης λογαριασμού
resend_account_activation_email_success: Αν υπάρχει λογαριασμός που συσχετίζεται
μ' αυτό το email, θα στείλουμε νέο email ενεργοποίησης.
ignore_magazines_custom_css: Αγνόηση προσαρμοσμένης CSS περιοδικών
oauth.consent.title: Φόρμα Συναίνεσης OAuth2
oauth.consent.grant_permissions: Αποδοχή Δικαιωμάτων
oauth.consent.deny: Απόρριψη
oauth.client_identifier.invalid: Μη Έγκυρο ID Πελάτη OAuth!
oauth.client_not_granted_message_read_permission: Αυτή η εφαρμογή δεν έχει το
δικαίωμα να διαβάζει τα μηνύματά σου.
restrict_oauth_clients: Περιορισμός δημιουργίας Πελάτη OAuth2 για τους
Διαχειριστές
private_instance: Υποχρέωσε τους χρήστες να συνδεθούν πριν μπορούν να έχουν
πρόσβαση σε περιεχόμενο
oauth2.grant.entry.vote: Ψήφισε θετικά ή αρνητικά, ενίσχυσε οποιοδήποτε νήμα.
oauth2.grant.entry.report: Ανέφερε οποιοδήποτε νήμα.
oauth2.grant.entry_comment.create: Δημιούργησε νέα σχόλια σε νήματα.
oauth2.grant.domain.block: Απέκλεισε τομείς ή αίρε τον αποκλεισμό και δες τους
τομείς που έχεις αποκλείσει.
oauth2.grant.subscribe.general: Κάνε εγγραφή ή ακολούθησε οποιοδήποτε περιοδικό,
τομέα ή χρήστη και δες τα περιοδικά, τους τομείς και τους χρήστες που
εγγράφεσαι.
oauth2.grant.moderate.magazine.reports.all: Διαχειρίσου αναφορές στα περιοδικά
που συντονίζεις.
oauth2.grant.domain.all: Εγγράψου σε τομείς ή απέκλεισέ τους και δες τους τομείς
στους οποίους έχεις εγγραφεί ή αποκλείσει.
oauth2.grant.domain.subscribe: Εγγράψου ή κάνε απεγγραφή σε τομείς και δες τους
τομείς στους οποίους έχεις εγγραφεί.
oauth2.grant.entry.edit: Επεξεργάσου τα υπάρχοντα σου νήματα.
oauth2.grant.moderate.magazine_admin.stats: Δες το περιεχόμενο, τις ψήφους και
τα στατιστικά προβολής των ιδιόκτητων περιοδικών σου.
oauth2.grant.entry_comment.edit: Επεξεργάσου τα υπάρχοντα σχόλιά σου σε νήματα.
oauth2.grant.moderate.magazine_admin.delete: Διέγραψε κάποιο απ' τα ιδιότητα
περιοδικά σου.
oauth2.grant.moderate.magazine.trash.read: Προβολή περιεχομένου απορριμάτων στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.magazine_admin.edit_theme: Επεξεργάσου την προσαρμοσμένη
CSS οποιωνδήποτε από τα ιδιόκτητα περιοδικά σου.
oauth2.grant.moderate.magazine_admin.moderators: Πρόσθεσε ή αφαίρεσε συντονιστές
από οποιοδήποτε από τα ιδιόκτητα περιοδικά σου.
oauth2.grant.entry_comment.delete: Διέγραψε τα υπάρχοντα σχόλιά σου σε νήματα.
oauth2.grant.entry.create: Δημιούργησε νέα νήματα.
unblock: Άρση αποκλεισμού
oauth2.grant.moderate.magazine.ban.delete: Άρση αποκλεισμού χρηστών σε περιοδικά
που συντονίζεις.
oauth2.grant.moderate.magazine.list: Διάβασε μια λίστα με τα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.magazine.reports.read: Διάβασε αναφορές στα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.magazine.reports.action: Αποδέξου ή απέρριψε αναφορές στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.magazine_admin.create: Δημιούργησε νέα περιοδικά.
oauth2.grant.moderate.magazine_admin.update: Επεξεργάσου οποιονδήποτε από τους
κανόνες, την περιγραφή, τη κατάσταση NSFW ή το εικονίδιο των ιδιόκτητων
περιοδικών σου.
oauth2.grant.moderate.magazine_admin.all: Δημιούργησε, επεξεργάσου ή διέγραψε τα
ιδιόκτητα περιοδικά σου.
oauth2.grant.moderate.magazine_admin.badges: Δημιούργησε ή αφαίρεσε τα σήματα
από τα ιδιόκτητα περιοδικά σου.
oauth2.grant.moderate.magazine_admin.tags: Δημιούργησε ή αφαίρεσε ετικέτες από
τα ιδιόκτητα περιοδικά σου.
oauth2.grant.admin.all: Εκτέλεσε οποιαδήποτε ενέργεια διαχειριστή στην οντότητά
σου.
oauth2.grant.admin.entry.purge: Διέγραψε πλήρως οποιοδήποτε νήμα από την
οντότητά σου.
oauth2.grant.read.general: Διάβασε όλο το περιεχόμενο στο οποίο έχεις πρόσβαση.
oauth2.grant.write.general: Δημιούργησε ή επεξεργάσου οποιοδήποτε από τα νήματα,
αναρτήσεις ή σχόλια σου.
oauth2.grant.delete.general: Διάγραψε οποιοδήποτε από τα νήματα, αναρτήσεις ή
σχόλια σου.
oauth2.grant.report.general: Ανάφερε νήματα, αναρτήσεις ή σχόλια.
oauth2.grant.vote.general: Δώσε θετική, αρνητική ψήφο ή ενίσχυσε νήματα,
αναρτήσεις ή σχόλια.
oauth2.grant.block.general: Απέκλεισε ή αίρε τον αποκλεισμό οποιουδήποτε
περιοδικού, τομέα ή χρήστη και δες τα περιοδικά, τους τομείς και τους χρήστες
που έχεις αποκλείσει.
oauth2.grant.entry.all: Δημιούργησε, επεξεργάσου ή διάγραψε τα νήματα σου και
ψήφισε, ενίσχυσε ή ανέφερε οποιοδήποτε νήμα.
oauth2.grant.entry.delete: Διάγραψε τα υπάρχοντα σου νήματα.
oauth2.grant.entry_comment.all: Δημιούργησε, επεξεργάσου ή διάγραψε τα σχόλιά
σου σε νήματα και ψήφισε, ενίσχυσε ή ανέφερε οποιοδήποτε σχόλιο σε ένα νήμα.
downvotes_mode: Λειτουργία αρνητικών ψήφων
change_downvotes_mode: Αλλαγή λειτουργίας αρνητικών ψήφων
hidden: Κρυφό
enabled: Ενεργό
disabled: Ανενεργό
oauth2.grant.magazine.block: Απέκλεισε ή κατάργησε τον αποκλεισμό περιοδικών και
προβολή των περιοδικών που έχεις αποκλείσει.
oauth2.grant.post.edit: Επεξεργάσου υπάρχουσες αναρτήσεις.
oauth2.grant.post.delete: Διέγραψε τις υπάρχουσες αναρτήσεις σου.
oauth2.grant.magazine.subscribe: Εγγράψου ή κατάργησε την εγγραφή σου σε
περιοδικά και δες τα περιοδικά στα οποία έχεις εγγραφεί.
oauth2.grant.entry_comment.vote: Δώσε θετική ή αρνητική ψήφο ή ενίσχυσε
οποιοδήποτε σχόλιο σ' ένα νήμα.
oauth2.grant.entry_comment.report: Ανέφερε οποιοδήποτε σχόλιο σ' ένα νήμα.
oauth2.grant.magazine.all: Κάνε εγγραφή ή απέκλεισε περιοδικά και δες τα
περιοδικά στα οποία έχεις εγγραφεί ή απέκλεισέ τα.
oauth2.grant.post.create: Δημιούργησε νέες αναρτήσεις.
oauth2.grant.post.all: Δημιούργησε, επεξεργάσου ή διέγραψε τα μικροϊστολόγιά σου
και ψήφισε, ενίσχυσε ή ανέφερε οποιοδήποτε μικροϊστολόγιο.
oauth2.grant.post.vote: Ψήφισε θετικά ή αρνητικά, ενίσχυσε οποιαδήποτε ανάρτηση.
oauth2.grant.post.report: Ανέφερε οποιαδήποτε ανάρτηση.
oauth2.grant.post_comment.edit: Επεξεργάσου τα υπάρχοντα σχόλιά σου σε
αναρτήσεις.
oauth2.grant.post_comment.delete: Διέγραψε τα υπάρχοντα σχόλιά σου σε
αναρτήσεις.
oauth2.grant.post_comment.all: Δημιούργησε, επεξεργάσου ή διέγραψε τα σχόλιά σου
σε αναρτήσεις και ψήφισε, ενίσχυσε ή να ανέφερε οποιοδήποτε σχόλιο σε με μια
ανάρτηση.
oauth2.grant.post_comment.create: Δημιούργησε νέα σχόλια σε αναρτήσεις.
oauth2.grant.user.message.create: Στείλε μηνύματα σε άλλους χρήστες.
oauth2.grant.user.notification.all: Διάβασε και καθάρισε τις ειδοποιήσεις σου.
oauth2.grant.user.notification.read: Διάβασε τις ειδοποιήσεις σου,
συμπεριλαμβανομένων αυτών για μηνύματα.
oauth2.grant.user.notification.delete: Καθάρισε τις ειδοποιήσεις σου.
toolbar.spoiler: Σπόιλερ
oauth2.grant.user.profile.all: Διάβασε και επεξεργάσου το προφίλ σου.
oauth2.grant.user.message.all: Διάβασε τα μηνύματά σου και στείλε μηνύματα σε
άλλους χρήστες.
oauth2.grant.user.profile.read: Διάβασε το προφίλ σου.
oauth2.grant.post_comment.vote: Ψήφισε θετικά ή αρητικά, ενίσχυσε οποιοδήποτε
σχόλιο σε μια ανάρτηση.
oauth2.grant.post_comment.report: Ανέφερε οποιοδήποτε σχόλιο σε μια ανάρτηση.
oauth2.grant.user.profile.edit: Επεξεργάσου το προφίλ σου.
oauth2.grant.user.message.read: Διάβασε τα μηνύματά σου.
oauth2.grant.user.oauth_clients.all: Διάβασε και επεξεργάσου τα δικαιώματα που
έχεις χορηγήσει σε άλλες εφαρμογές OAuth2.
oauth2.grant.user.oauth_clients.read: Διάβασε τα δικαιώματα που έχεις χορηγήσει
σε άλλες εφαρμογές OAuth2.
oauth2.grant.user.all: Διάβασε και επεξεργάσου το προφίλ, τα μηνύματα ή τις
ειδοποιήσεις σου· διάβασε και επεξεργάσου δικαιώματα που έχεις δώσει σε άλλες
εφαρμογές, ακολούθησε ή μπλόκαρε άλλους χρήστες· δες λίστες των χρηστών που
ακολουθείς ή έχεις αποκλείσει.
oauth2.grant.user.oauth_clients.edit: Επεξεργάσου τα δικαιώματα που έχεις
χορηγήσει σε άλλες εφαρμογές OAuth2.
oauth2.grant.user.follow: Ακολούθησε ή αφαίρεσε από ακόλουθο χρήστες και διάβασε
μια λίστα χρηστών που ακολουθείς.
oauth2.grant.moderate.entry.all: Συντόνισε τα νήματα στα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.entry.change_language: Άλλαξε τη γλώσσα των νημάτων στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.all: Εκτέλεσε οποιαδήποτε ενέργεια συντονισμού που έχετε
την άδεια να εκτελέσετε στα περιοδικά που συντονίζεις.
oauth2.grant.user.block: Αποκλεισμός ή άρση αποκλεισμού χρήστη και δες την λίστα
χρηστών που έχεις αποκλείσει.
oauth2.grant.moderate.entry.pin: Καρφίτσωσε νήματα στην κορυφή των περιοδικών
που συντονίζεις.
oauth2.grant.moderate.entry.set_adult: Επισήμανση νημάτων ως NSFW στα περιοδικά
που συντονίζεις.
oauth2.grant.moderate.entry.trash: Απόρριψε ή ανάκτησε νήματα στα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.entry_comment.all: Συντόνισε τα σχόλια σε νήματα στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.entry_comment.change_language: Άλλαξε τη γλώσσα των
σχολίων σε νήματα στα περιοδικά που συντονίζεις.
oauth2.grant.moderate.entry_comment.set_adult: Επισημάνετε τα σχόλια στα νήματα
ως NSFW στα περιοδικά που συντονίζετε.
oauth2.grant.admin.post_comment.purge: Διέγραψε εντελώς οποιοδήποτε σχόλιο σε
μια ανάρτηση από την οντότητα σου.
oauth2.grant.admin.entry_comment.purge: Διέγραψε εντελώς οποιοδήποτε σχόλιο σε
ένα νήμα από την οντότητα σου.
oauth2.grant.admin.post.purge: Διέγραψε εντελώς οποιαδήποτε ανάρτηση από την
οντότητα σου.
oauth2.grant.moderate.entry_comment.trash: Πέταξε ή επανέφερε σχολίων σε νήματα
στα περιοδικά που διαχειρίζεσαι.
oauth2.grant.moderate.post.set_adult: Επισήμανε τις αναρτήσεις ως NSFW στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.post_comment.set_adult: Επισήμανε τα σχόλια σε αναρτήσεις
ως NSFW στα περιοδικά που συντονίζεις.
oauth2.grant.moderate.post_comment.all: Συντόνισε σχόλια σε αναρτήσεις στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.post_comment.change_language: Άλλαξε τη γλώσσα των σχολίων
σε αναρτήσεις στα περιοδικά που συντονίζεις.
oauth2.grant.moderate.magazine.ban.all: Διαχειρίσου τους αποκλεισμένους χρήστες
στα περιοδικά που συντονίζεις.
oauth2.grant.moderate.post.all: Συντόνισε αναρτήσεις στα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.post.change_language: Άλλαξε τη γλώσσα των αναρτήσεων στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.post.trash: Πέταξε ή επανέφερε τις αναρτήσεις στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.post_comment.trash: Πέταξε ή επανέφερε τα σχόλια σε
αναρτήσεις στα περιοδικά που συντονίζεις.
oauth2.grant.moderate.magazine.all: Διαχειρίσου τους αποκλεισμούς, τις αναφορές
και την προβολή αντικειμένων στον κάδο απορριμμάτων στα περιοδικά που
συντονίζεις.
oauth2.grant.moderate.magazine.ban.read: Δες τους αποκλεισμένους χρήστες στα
περιοδικά που συντονίζεις.
oauth2.grant.moderate.magazine.ban.create: Απέκλεισε τους χρήστες στα περιοδικά
που συντονίζεις.
comment_reply_position_help: Εμφανίζει τη φόρμα απάντησης σχολίων είτε στην
κορυφή ή στο κάτω μέρος της σελίδας. Όταν η "Άπειρη κύλιση" είναι ενεργή η
θέση θα είναι πάντα στην κορυφή.
flash_post_pin_success: Η ανάρτηση έχει καρφιτσωθεί επιτυχώς.
oauth2.grant.admin.magazine.purge: Πλήρης διαγραφή περιοδικών στην οντότητα σου.
oauth2.grant.admin.user.ban: Αποκλεισμός ή άρση αποκλεισμού χρηστών από την
οντότητά σου.
oauth2.grant.admin.user.verify: Επαλήθευσε τους χρήστες στην οντότητά σου.
oauth2.grant.admin.federation.update: Πρόσθεσε ή αφαίρεσε οντότητες στον ή από
τον κατάλογο των οντοτήτων εκτός ομοσπονδίας.
oauth2.grant.admin.oauth_clients.all: Δες ή ανακάλεσε τους πελάτες OAuth2 που
υπάρχουν στην οντότητα σου.
oauth2.grant.admin.user.all: Αποκλεισμός, επαλήθευση ή πλήρης διαγραφή των
χρηστών στην οντότητά σου.
flash_post_unpin_success: Η ανάρτηση έχει ξεκαρφιτσωθεί επιτυχώς.
show_avatars_on_comments: Εμφάνιση Άβαταρ Σχολίων
oauth2.grant.admin.magazine.move_entry: Μετακίνησε τα νήματα μεταξύ των
περιοδικών της οντότητας σου.
oauth2.grant.admin.instance.settings.read: Δες τις ρυθμίσεις στην οντότητά σου.
oauth2.grant.admin.oauth_clients.read: Δες τους πελάτες OAuth2 που υπάρχουν στην
οντότητα σου και τα στατιστικά χρήσης τους.
single_settings: Απλό
update_comment: Ενημέρωση σχολίου
oauth2.grant.admin.oauth_clients.revoke: Ανακάλεσε την πρόσβαση στους πελάτες
OAuth2 στην οντότητα σου.
last_active: Τελευταία Ενεργό
oauth2.grant.admin.instance.settings.edit: Ενημέρωσε τις ρυθμίσεις στην οντότητα
σου.
oauth2.grant.admin.instance.information.edit: Ενημέρωσε τα Σχετικά, FAQ,
Συμβόλαιο, Όρους Της Υπηρεσίας και τις σελίδες Πολιτικής Απορρήτου στην
οντότητά σου.
oauth2.grant.admin.magazine.all: Μετακίνησε νήματα μεταξύ ή διέγραψε εντελώς τα
περιοδικά στην οντότητα σου.
oauth2.grant.admin.user.delete: Διέγραψε χρήστες από την οντότητά σου.
oauth2.grant.admin.user.purge: Διέγραψε πλήρως τους χρήστες από την οντότητά
σου.
oauth2.grant.admin.instance.all: Δες και ενημέρωσε τις ρυθμίσεις οντότητας ή τις
πληροφορίες.
oauth2.grant.admin.instance.stats: Δες τα στατιστικά στοιχεία της οντότητας σου.
oauth2.grant.admin.instance.settings.all: Δες ή ενημέρωσε τις ρυθμίσεις στην
οντότητά σου.
oauth2.grant.admin.federation.all: Δες και ενημέρωσε τις τρέχουσες - εκτός
ομοσπονδίας, οντότητες.
oauth2.grant.admin.federation.read: Δες τη λίστα των οντοτήτων εκτός
ομοσπονδίας.
moderation.report.ban_user_description: Θες να αποκλείσεις τον χρήστη
(%username%) που δημιούργησε αυτό το περιεχόμενο από αυτό το περιοδικό;
subject_reported_exists: Το περιεχόμενο αυτό έχει ήδη αναφερθεί.
moderation.report.ban_user_title: Αποκλεισμός Χρήστη
magazine_theme_appearance_custom_css: Το Custom CSS που θα ισχύει κατά την
προβολή περιεχομένου εντός του περιοδικού σου.
moderation.report.approve_report_title: Έγκριση Αναφοράς
moderation.report.reject_report_title: Απόρριψη Αναφοράς
purge_content: Εκκαθάριση περιεχομένου
delete_content: Διαγραφή περιεχομένου
show_avatars_on_comments_help: Εμφάνιση/απόκρυψη άβαταρ χρήστη κατά την προβολή
σχολίων σε ένα ενιαίο νήμα ή ανάρτηση.
comment_reply_position: Θέση απάντησης σχολίου
magazine_theme_appearance_icon: Προσαρμοσμένο εικονίδιο για το περιοδικό. Αν δεν
επιλεχθεί κανένα, θα χρησιμοποιηθεί το προεπιλεγμένο.
magazine_theme_appearance_background_image: Προσαρμοσμένη εικόνα παρασκηνίου που
θα εφαρμοστεί κατά την προβολή περιεχομένου εντός του περιοδικού σου.
moderation.report.approve_report_confirmation: Σίγουρα θες να εγκρίνεις αυτή την
αναφορά;
moderation.report.reject_report_confirmation: Σίγουρα θες να απορρίψεις αυτή την
αναφορά;
oauth2.grant.moderate.post.pin: Καρφίτσωσε στην κορυφή των περιοδικών που
συντονίζεις.
cancel: Ακύρωση
subscription_sort: Ταξινόμηση
2fa.qr_code_img.alt: Ένας κωδικός QR που επιτρέπει τη ρύθμιση ταυτότητας δύο
παραγόντων για τον λογαριασμό σου
two_factor_backup: Εφεδρικό κωδικοί ταυτοποίησης δύο παραγόντων
2fa.authentication_code.label: Κωδικός Ταυτοποίησης
2fa.remove: Αφαίρεση 2FA
2fa.disable: Απενεργοποίηση ταυτοποίησης δύο παραγόντων
2fa.backup: Οι εφεδρικό κωδικοί ταυτοποίησης δύο παραγόντων σου
2fa.backup_codes.recommendation: Συνιστάται να κρατήσεις ένα αντίγραφο αυτών σε
ασφαλή θέση.
flash_account_settings_changed: Οι ρυθμίσεις λογαριασμού σου έχουν αλλάξει με
επιτυχία. Θα χρειαστεί να συνδεθείς ξανά.
show_subscriptions: Εμφάνιση εγγραφών
subscription_panel_large: Μεγάλο πάνελ
delete_account_desc: Διέγραψε το λογαριασμό, συμπεριλαμβανομένων των απαντήσεων
άλλων χρηστών σε δημιουργημένα νήματα, αναρτήσεις και σχόλια.
2fa.code_invalid: Ο κωδικός ταυτοποίησης δεν είναι έγκυρος
2fa.enable: Ρύθμιση ταυτοποίησης δύο παραγόντων
subscription_sidebar_pop_out_left: Μετακίνηση σε ξεχωριστή sidebar στα αριστερά
2fa.setup_error: Σφάλμα ενεργοποίησης 2FA για λογαριασμό
purge_content_desc: Πλήρης εκκαθάριση του περιεχομένου του χρήστη,
συμπεριλαμβανομένης της διαγραφής των απαντήσεων άλλων χρηστών σε
δημιουργημένα νήματα, αναρτήσεις και σχόλια.
delete_content_desc: Διέγραψε το περιεχόμενο του χρήστη, αφήνοντας τις
απαντήσεις άλλων χρηστών στα δημιουργημένα νήματα, αναρτήσεις και σχόλια.
schedule_delete_account: Προγραμματισμός Διαγραφής
schedule_delete_account_desc: Προγραμμάτισε τη διαγραφή αυτού του λογαριασμού σε
30 ημέρες. Αυτό θα αποκρύψει τον χρήστη και το περιεχόμενό του, καθώς και θα
εμποδίσει τον χρήστη να συνδεθεί.
remove_schedule_delete_account: Αφαίρεση Προγραμματισμένης Διαγραφής
remove_schedule_delete_account_desc: Αφαίρεσε την προγραμματισμένη διαγραφή. Όλο
το περιεχόμενο θα είναι διαθέσιμο ξανά και ο χρήστης θα είναι σε θέση να
συνδεθεί.
two_factor_authentication: Έλεγχος ταυτότητας δύο παραγόντων
2fa.verify: Επαλήθευση
2fa.backup-create.help: Μπορείτε να δημιουργήσεις νέους εφεδρικούς κωδικούς
αυθεντικοποίησης· αυτό θα ακυρώσει τους υφιστάμενους κωδικούς.
2fa.add: Προσθήκη στον λογαριασμό μου
2fa.backup-create.label: Δημιουργία νέων εφεδρικών κωδικών ταυτοποίησης
2fa.qr_code_link.title: Η επίσκεψη σε αυτόν τον σύνδεσμο μπορεί να επιτρέψει
στην πλατφόρμα σου να καταχωρήσει αυτή την επαλήθευση δύο παραγόντων
2fa.user_active_tfa.title: Ο χρήστης έχει ενεργό 2FA
2fa.backup_codes.help: Μπορείς να χρησιμοποιήσεις αυτούς τους κωδικούς όταν δεν
έχεις τη συσκευή επαλήθευσης δύο παραγόντων ή εφαρμογή σου. Δεν θα
τους δείξεις ξανά και θα μπορείς να χρησιμοποιήσεις τον καθένα τους
μόνο μία φορά .
password_and_2fa: Κωδικός & 2FA
alphabetically: Αλφαβητικά
subscriptions_in_own_sidebar: Σε ξεχωριστή sidebar
sidebars_same_side: Sidebars στην ίδια πλευρά
subscription_sidebar_pop_out_right: Μετακίνηση σε ξεχωριστή sidebar στα δεξιά
subscription_sidebar_pop_in: Μετακίνηση συνδρομών στο inline πάνελ
2fa.available_apps: Χρησιμοποίησε μια εφαρμογή επαλήθευσης δύο παραγόντων, όπως
το %google_authenticator%, %aegis% (Android) ή το %raivo% (iOS) για να
σαρώσεις τον κωδικό QR.
your_account_is_not_yet_approved: Ο λογαριασμός σου δεν έχει εγκριθεί ακόμα. Θα
σου στείλουμε ένα email μόλις οι διαχειριστές έχουν επεξεργαστεί το αίτημα
εγγραφής σου.
subscription_header: Εγγεγραμμένα Περιοδικά
close: Κλείσιμο
notify_on_user_signup: Νέες εγγραφές
2fa.verify_authentication_code.label: Εισήγαγε έναν κωδικό δύο παραγόντων για να
επαληθεύσεις τη ρύθμιση
position_bottom: Κάτω μέρος
position_top: Επάνω
pending: Σε αναμονή
flash_thread_new_error: Δεν μπόρεσε να δημιουργηθεί νήμα. Κάτι πήγε στραβά.
remove_user_avatar: Αφαίρεση άβαταρ
remove_user_cover: Αφαίρεση εξωφύλλου
oauth2.grant.user.bookmark_list: Διάβασε, επεξεργάσου και διέγραψε τις λίστες
σελιδοδεικτών σου
oauth2.grant.user.bookmark_list.read: Διάβασε τις λίστες σελιδοδεικτών σου
oauth2.grant.user.bookmark_list.delete: Διέγραψε τις λίστες σελιδοδεικτών σου
flash_image_download_too_large_error: Η εικόνα δεν ήταν δυνατό να δημιουργηθεί,
είναι πολύ μεγάλη (μέγιστο μέγεθος %bytes%)
flash_email_was_sent: Το email εστάλη με επιτυχία.
flash_email_failed_to_sent: Το email δεν μπόρεσε να αποσταλεί.
viewing_one_signup_request: Βλέπεις μόνο ένα αίτημα εγγραφής από %username%
flash_post_new_success: Η ανάρτηση δημιουργήθηκε με επιτυχία.
oauth2.grant.user.bookmark: Προσθήκη και αφαίρεση σελιδοδεικτών
oauth2.grant.user.bookmark.remove: Αφαίρεση σελιδοδεικτών
oauth2.grant.user.bookmark.add: Προσθήκη σελιδοδεικτών
oauth2.grant.user.bookmark_list.edit: Επεξεργάσου τις λίστες σελιδοδεικτών σου
flash_magazine_theme_changed_success: Η εμφάνιση του περιοδικού ενημερώθηκε με
επιτυχία.
flash_post_new_error: Η ανάρτηση δεν μπόρεσε να δημιουργηθεί. Κάτι πήγε στραβά.
flash_thread_tag_banned_error: Δε μπόρεσε να δημιουργηθεί νήμα. Το περιεχόμενο
δεν επιτρέπεται.
page_width_auto: Αυτόματο
filter_labels: Φιλτράρισμα Ετικετών
2fa.manual_code_hint: Εάν δεν μπορείς να σαρώσεις τον κωδικό QR, εισήγαγε το
μυστικό χειροκίνητα
flash_magazine_theme_changed_error: Αποτυχία ενημέρωσης εμφάνισης του
περιοδικού.
flash_comment_new_success: Το σχόλιο δημιουργήθηκε επιτυχώς.
flash_user_settings_general_success: Οι ρυθμίσεις χρήστη αποθηκεύτηκαν με
επιτυχία.
flash_user_settings_general_error: Αποτυχία αποθήκευσης ρυθμίσεων χρήστη.
flash_user_edit_profile_success: Οι ρυθμίσεις προφίλ χρήστη αποθηκεύτηκαν με
επιτυχία.
flash_comment_edit_success: Το σχόλιο ενημερώθηκε επιτυχώς.
flash_comment_new_error: Αποτυχία δημιουργίας σχολίου. Κάτι πήγε στραβά.
flash_user_edit_email_error: Αποτυχία αλλαγής email.
flash_user_edit_password_error: Αποτυχία αλλαγής κωδικού πρόσβασης.
flash_thread_edit_error: Αποτυχία επεξεργασίας νήματος. Κάτι πήγε στραβά.
flash_post_edit_error: Αποτυχία επεξεργασίας ανάρτησης. Κάτι πήγε στραβά.
flash_post_edit_success: Η ανάρτηση επεξεργάστηκε με επιτυχία.
page_width: Πλάτος σελίδας
page_width_fixed: Σταθερό
auto: Αυτόματο
open_url_to_fediverse: Άνοιγμα πρωτότυπου URL
page_width_max: Μεγ
flash_comment_edit_error: Αποτυχία επεξεργασίας σχολίου. Κάτι πήγε στραβά.
flash_user_edit_profile_error: Αποτυχία αποθήκευσης ρυθμίσεων προφίλ.
change_my_cover: Άλλαξε το εξώφυλλό μου
change_my_avatar: Άλλαξε το άβατάρ μου
account_settings_changed: Οι ρυθμίσεις λογαριασμού σου έχουν αλλάξει με
επιτυχία. Θα χρειαστεί να συνδεθείς ξανά.
magazine_deletion: Διαγραφή περιοδικού
toolbar.emoji: Εμότζι
edit_my_profile: Επεξεργασία του προφίλ μου
type_search_term_url_handle: Τύπος αναζήτησης, διεύθυνση ή handle
delete_magazine: Διαγραφή περιοδικού
restore_magazine: Επαναφορά περιοδικού
purge_magazine: Εκκαθάριση περιοδικού
magazine_is_deleted: Το περιοδικό διαγράφεται. Μπορείς να κάνεις επαναφορά μέσα σε 30 ημέρες.
suspend_account: Αναστολή λογαριασμού
unsuspend_account: Άρση αναστολής λογαριασμού
account_suspended: Ο λογαριασμός έχει ανασταλεί.
account_unsuspended: Ο λογαριασμός δεν είναι πλέον σε αναστολή.
deletion: Διαγραφή
user_suspend_desc: Η αναστολή του λογαριασμού σου κρύβει το περιεχόμενό σου από
την οντότητα, αλλά δεν τον αφαιρεί μόνιμα και μπορείς να το επαναφέρεις ανά
πάσα στιγμή.
account_banned: Ο λογαριασμός έχει αποκλειστεί.
================================================
FILE: translations/messages.en.yaml
================================================
type.link: Link
type.article: Thread
type.photo: Photo
type.video: Video
type.smart_contract: Smart contract
type.magazine: Magazine
thread: Thread
threads: Threads
microblog: Microblog
people: People
events: Events
magazine: Magazine
magazines: Magazines
search: Search
add: Add
select_channel: Select a channel
login: Log in
sort_by: Sort by
top: Top
hot: Hot
active: Active
newest: Newest
oldest: Oldest
commented: Commented
change_view: Change view
filter_by_time: Filter by time
filter_by_type: Filter by type
filter_by_subscription: Filter by subscription
filter_by_federation: Filter by federation status
comments_count: '{0}Comments|{1}Comment|]1,Inf[ Comments'
subscribers_count: '{0}Subscribers|{1}Subscriber|]1,Inf[ Subscribers'
followers_count: '{0}Followers|{1}Follower|]1,Inf[ Followers'
marked_for_deletion: Marked for deletion
marked_for_deletion_at: Marked for deletion at %date%
favourites: Upvotes
favourite: Favorite
more: More
avatar: Avatar
added: Added
up_votes: Boosts
down_votes: Reduces
no_comments: No comments
created_at: Created
created_since: Created since
owner: Owner
subscribers: Subscribers
online: Online
comments: Comments
posts: Posts
replies: Replies
moderators: Moderators
mod_log: Moderation log
add_comment: Add comment
add_post: Add post
add_media: Add media
remove_media: Remove media
remove_user_avatar: Remove avatar
remove_user_cover: Remove cover
markdown_howto: How does the editor work?
enter_your_comment: Enter your comment
enter_your_post: Enter your post
activity: Activity
cover: Cover
related_posts: Related posts
random_posts: Random posts
federated_magazine_info: This magazine is from a federated server and may be
incomplete.
disconnected_magazine_info: This magazine is not receiving updates (last
activity %days% day(s) ago).
always_disconnected_magazine_info: This magazine is not receiving updates.
subscribe_for_updates: Subscribe to start receiving updates.
federated_user_info: This profile is from a federated server and may be
incomplete.
go_to_original_instance: View on remote instance
empty: Empty
subscribe: Subscribe
unsubscribe: Unsubscribe
follow: Follow
unfollow: Unfollow
reply: Reply
login_or_email: Login or email
password: Password
remember_me: Remember me
dont_have_account: Don't have an account?
you_cant_login: Forgot your password?
already_have_account: Already have an account?
register: Register
reset_password: Reset password
show_more: Show more
to: to
in: in
from: from
username: Username
displayname: Display name
email: Email
repeat_password: Repeat password
agree_terms: Consent to %terms_link_start%Terms and Conditions%terms_link_end%
and %policy_link_start%Privacy Policy%policy_link_end%
terms: Terms of service
privacy_policy: Privacy policy
about_instance: About
all_magazines: All magazines
stats: Statistics
fediverse: Fediverse
create_new_magazine: Create new magazine
add_new_article: Add new thread
add_new_link: Add new link
add_new_photo: Add new photo
add_new_post: Add new post
add_new_video: Add new video
contact: Contact
faq: FAQ
rss: RSS
change_theme: Change theme
downvotes_mode: Downvotes mode
change_downvotes_mode: Change downvotes mode
disabled: Disabled
hidden: Hidden
enabled: Enabled
useful: Useful
help: Help
check_email: Check your email
reset_check_email_desc: If there is already an account associated with your
email address, you should receive an email shortly containing a link that you
can use to reset your password. This link will expire in %expire%.
reset_check_email_desc2: If you don't receive an email please check your spam
folder.
try_again: Try again
up_vote: Boost
down_vote: Reduce
email_confirm_header: Hello! Confirm your email address.
email_confirm_content: 'Ready to activate your Mbin account? Click on the link below:'
email_verify: Confirm email address
email_confirm_expire: Please note that the link will expire in an hour.
email_confirm_title: Confirm your email address.
select_magazine: Select a magazine
add_new: Add new
url: URL
title: Title
body: Body
tags: Tags
tag: Tag
badges: Badges
is_adult: 18+ / NSFW
eng: ENG
oc: OC
image: Image
image_alt: Image alternative text
name: Name
description: Description
rules: Rules
domain: Domain
followers: Followers
following: Following
subscriptions: Subscriptions
overview: Overview
cards: Cards
columns: Columns
user: User
joined: Joined
moderated: Moderated
people_local: Local
people_federated: Federated
reputation_points: Reputation points
related_tags: Related tags
go_to_content: Go to content
go_to_filters: Go to filters
go_to_search: Go to search
subscribed: Subscribed
all: All
logout: Log out
classic_view: Classic view
compact_view: Compact view
chat_view: Chat view
tree_view: Tree view
table_view: Table view
cards_view: Cards view
3h: 3h
6h: 6h
12h: 12h
1d: 1d
1w: 1w
1m: 1m
1y: 1y
links: Links
articles: Threads
photos: Photos
videos: Videos
report: Report
share: Share
copy_url: Copy Mbin URL
copy_url_to_fediverse: Copy original URL
share_on_fediverse: Share on Fediverse
crosspost: Crosspost
edit: Edit
are_you_sure: Are you sure?
moderate: Moderate
reason: Reason
edit_entry: Edit thread
delete: Delete
edit_post: Edit post
edit_comment: Save changes
menu: Menu
settings: Settings
general: General
profile: Profile
blocked: Blocked
reports: Reports
notifications: Notifications
messages: Messages
appearance: Appearance
homepage: Homepage
hide_adult: Hide NSFW content
featured_magazines: Featured magazines
privacy: Privacy
show_profile_subscriptions: Show magazine subscriptions
show_profile_followings: Show following users
notify_on_new_entry_reply: Any level comments in threads I authored
notify_on_new_entry_comment_reply: Replies to my comments in any threads
notify_on_new_post_reply: Any level replies to posts I authored
notify_on_new_post_comment_reply: Replies to my comments on any posts
notify_on_new_entry: New threads (links or articles) in any magazine to which
I'm subscribed
notify_on_new_posts: New posts in any magazine to which I'm subscribed
notify_on_user_signup: New signups
save: Save
about: About
old_email: Current email
new_email: New email
new_email_repeat: Confirm new email
current_password: Current password
new_password: New password
new_password_repeat: Confirm new password
change_email: Change email
change_password: Change password
expand: Expand
collapse: Collapse
domains: Domains
error: Error
votes: Votes
theme: Theme
dark: Dark
light: Light
solarized_light: Solarized Light
solarized_dark: Solarized Dark
default_theme: Default theme
default_theme_auto: Light/Dark (Auto Detect)
solarized_auto: Solarized (Auto Detect)
font_size: Font size
size: Size
boosts: Boosts
show_users_avatars: 'Show users’ avatars'
yes: Yes
no: No
show_magazines_icons: 'Show magazines’ icons'
show_thumbnails: Show thumbnails
rounded_edges: Rounded edges
removed_thread_by: has removed a thread by
restored_thread_by: has restored a thread by
removed_comment_by: has removed a comment by
restored_comment_by: has restored comment by
removed_post_by: has removed a post by
restored_post_by: has restored a post by
he_banned: banned
he_unbanned: unbanned
read_all: Read all
show_all: Show all
flash_register_success: Welcome aboard! Your account is now registered. One last
step - check your inbox for an activation link that will bring your account to
life.
flash_thread_new_success: The thread has been created successfully and is now
visible to other users.
flash_thread_edit_success: The thread has been successfully edited.
flash_thread_delete_success: The thread has been successfully deleted.
flash_thread_pin_success: The thread has been successfully pinned.
flash_thread_unpin_success: The thread has been successfully unpinned.
flash_magazine_new_success: The magazine has been created successfully. You can
now add new content or explore the magazine's administration panel.
flash_magazine_edit_success: The magazine has been successfully edited.
flash_mark_as_adult_success: The post has been successfully marked as NSFW.
flash_unmark_as_adult_success: The post has been successfully unmarked as NSFW.
too_many_requests: Limit exceeded, please try again later.
set_magazines_bar: Magazines bar
set_magazines_bar_desc: add the magazine names after the comma
set_magazines_bar_empty_desc: if the field is empty, active magazines are
displayed on the bar.
mod_log_alert: WARNING - The Modlog may contain unpleasant or distressing
content that has been removed by moderators. Please exercise caution.
added_new_thread: Added a new thread
edited_thread: Edited a thread
mod_remove_your_thread: A moderator removed your thread
added_new_comment: Added a new comment
edited_comment: Edited a comment
replied_to_your_comment: Replied to your comment
mod_deleted_your_comment: A moderator deleted your comment
added_new_post: Added a new post
edited_post: Edited a post
mod_remove_your_post: A moderator removed your post
added_new_reply: Added a new reply
wrote_message: Wrote a message
banned: Banned you
removed: Removed by mod
deleted: Deleted by author
mentioned_you: Mentioned you
comment: Comment
post: Post
parent_post: Parent Post
ban_expired: Ban expired
ban_expires: Ban expires
purge: Purge
send_message: Send direct message
message: Message
infinite_scroll: Infinite scrolling
show_top_bar: Show top bar
sticky_navbar: Sticky navbar
subject_reported: Content has been reported.
sidebar_position: Sidebar position
left: Left
right: Right
federation: Federation
status: Status
on: On
off: Off
instances: Instances
upload_file: Upload file
from_url: From url
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
add_moderator: Add moderator
add_badge: Add badge
bans: Bans
created: Created
expires: Expires
perm: Permanent
expired_at: Expired at
add_ban: Add ban
trash: Trash
icon: Icon
banner: Banner
done: Done
pin: Pin
unpin: Unpin
change_magazine: Change magazine
change_language: Change language
mark_as_adult: Mark NSFW
unmark_as_adult: Unmark NSFW
change: Change
pinned: Pinned
preview: Preview
article: Thread
reputation: Reputation
note: Note
writing: Writing
users: Users
content: Content
week: Week
weeks: Weeks
month: Month
months: Months
year: Year
federated: Federated
local: Local
admin_panel: Admin panel
dashboard: Dashboard
contact_email: Contact email
meta: Meta
instance: Instance
pages: Pages
FAQ: FAQ
type_search_term: Type search term
type_search_term_url_handle: Type search term, url or handle
federation_enabled: Federation enabled
registrations_enabled: Registration enabled
registration_disabled: Registration disabled
restore: Restore
add_mentions_entries: Add mention tags in threads
add_mentions_posts: Add mention tags in posts
Password is invalid: Password is invalid.
Your account is not active: Your account is not active.
Your account has been banned: Your account has been banned.
firstname: First name
send: Send
active_users: Active people
random_entries: Random threads
related_entries: Related threads
delete_account: Delete account
purge_account: Purge account
ban_account: Ban account
unban_account: Unban account
related_magazines: Related magazines
random_magazines: Random magazines
magazine_panel_tags_info: Provide only if you want content from the fediverse to
be included in this magazine based on tags
sidebar: Sidebar
auto_preview: Auto media preview
dynamic_lists: Dynamic lists
banned_instances: Banned instances
kbin_intro_title: Explore the Fediverse
kbin_intro_desc: is a decentralized platform for content aggregation and
microblogging that operates within the Fediverse network.
kbin_promo_title: Create your own instance
kbin_promo_desc: '%link_start%Clone repo%link_end% and develop fediverse'
captcha_enabled: Captcha enabled
header_logo: Header logo
browsing_one_thread: You are only browsing one thread in the discussion! All
comments are available on the post page.
viewing_one_signup_request: You are only viewing one signup request by
%username%
return: Return
boost: Boost
mercure_enabled: Mercure enabled
report_issue: Report issue
tokyo_night: Tokyo Night
preferred_languages: Filter languages of threads and posts
infinite_scroll_help: Automatically load more content when you reach the bottom
of the page.
sticky_navbar_help: The navbar will stick to the top of the page when you scroll
down.
auto_preview_help: Show the media (photo, video) previews in a larger size below
the content.
reload_to_apply: Reload page to apply changes
filter.origin.label: Choose origin
filter.fields.label: Choose which fields to search
filter.adult.label: Choose whether to display NSFW
filter.adult.hide: Hide NSFW
filter.adult.show: Show NSFW
filter.adult.only: Only NSFW
local_and_federated: Local and federated
filter.fields.only_names: Only names
filter.fields.names_and_descriptions: Names and descriptions
kbin_bot: Mbin Agent
bot_body_content: "Welcome to the Mbin Agent! This agent plays a crucial role in enabling
ActivityPub functionality within Mbin. It ensures that Mbin can communicate and
federate with other instances in the fediverse.\n\nActivityPub is an open standard
protocol that allows decentralized social networking platforms to communicate and
interact with each other. It enables users on different instances (servers) to follow,
interact with, and share content across the federated social network known as the
fediverse. It provides a standardized way for users to publish content, follow other
users, and engage in social interactions such as liking, sharing, and commenting
on threads or posts."
password_confirm_header: Confirm your password change request.
your_account_is_not_active: Your account has not been activated. Please check
your email for account activation instructions or request a new account activation email.
your_account_has_been_banned: Your account has been banned
your_account_is_not_yet_approved: Your account has not been approved yet. We
will send you an email as soon as the admins have processed your signup
request.
toolbar.bold: Bold
toolbar.italic: Italic
toolbar.strikethrough: Strikethrough
toolbar.header: Header
toolbar.quote: Quote
toolbar.code: Code
toolbar.link: Link
toolbar.image: Image
toolbar.unordered_list: Unordered List
toolbar.ordered_list: Ordered List
toolbar.mention: Mention
toolbar.spoiler: Spoiler
toolbar.emoji: Emoji
federation_page_enabled: Federation page enabled
federation_page_allowed_description: Known instances we federate with
federation_page_disallowed_description: Instances we do not federate with
federation_page_dead_title: Dead instances
federation_page_dead_description: Instances that we could not deliver at least
10 activities in a row and where the last successful deliver and -receive were
more than a week ago
federated_search_only_loggedin: Federated search limited if not logged in
account_deletion_title: Account deletion
account_deletion_description: Your account will be deleted in 30 days unless you
choose to delete the account immediately. To restore your account within 30
days, login with the same user credentials or contact an administrator.
account_deletion_button: Delete Account
account_deletion_immediate: Delete immediately
more_from_domain: More from domain
errors.server500.title: 500 Internal Server Error
errors.server500.description: Sorry, something went wrong on our end. If you
continue to see this error, try contacting the instance owner. If this
instance is not working at all, check out %link_start%other Mbin
instances%link_end% in the meanwhile until the problem is resolved.
errors.server429.title: 429 Too Many Requests
errors.server404.title: 404 Not found
errors.server403.title: 403 Forbidden
email_confirm_button_text: Confirm your password change request
email_confirm_link_help: Alternatively you can copy and paste the following into
your browser
email.delete.title: User account deletion request
email.delete.description: The following user has requested that their account be
deleted
resend_account_activation_email_question: Inactive account?
resend_account_activation_email: Re-send account activation email
resend_account_activation_email_error: There was an issue submitting this
request. There may be no account associated with that email or perhaps it is
already activated.
resend_account_activation_email_success: If an account associated with that
email exists, we will send out a new activation email.
resend_account_activation_email_description: Enter the email address associated
with your account. We will send out another activation email for you.
custom_css: Custom CSS
ignore_magazines_custom_css: Ignore magazines custom CSS
oauth.consent.title: OAuth2 Consent Form
oauth.consent.grant_permissions: Grant Permissions
oauth.consent.app_requesting_permissions: would like to perform the following
actions on your behalf
oauth.consent.app_has_permissions: can already perform the following actions
oauth.consent.to_allow_access: To allow this access, click the 'Allow' button
below
oauth.consent.allow: Allow
oauth.consent.deny: Deny
oauth.client_identifier.invalid: Invalid OAuth Client ID!
oauth.client_not_granted_message_read_permission: This app has not received
permission to read your messages.
restrict_oauth_clients: Restrict OAuth2 Client creation to Admins
private_instance: Force users to login before they can access any content
block: Block
unblock: Unblock
oauth2.grant.moderate.magazine.ban.delete: Unban users in your moderated
magazines.
oauth2.grant.moderate.magazine.list: Read a list of your moderated magazines.
oauth2.grant.moderate.magazine.reports.all: Manage reports in your moderated
magazines.
oauth2.grant.moderate.magazine.reports.read: Read reports in your moderated
magazines.
oauth2.grant.moderate.magazine.reports.action: Accept or reject reports in your
moderated magazines.
oauth2.grant.moderate.magazine.trash.read: View trashed content in your
moderated magazines.
oauth2.grant.moderate.magazine_admin.all: Create, edit, or delete your owned
magazines.
oauth2.grant.moderate.magazine_admin.create: Create new magazines.
oauth2.grant.moderate.magazine_admin.delete: Delete any of your owned magazines.
oauth2.grant.moderate.magazine_admin.update: Edit any of your owned magazines'
rules, description, NSFW status, or icon.
oauth2.grant.moderate.magazine_admin.edit_theme: Edit the custom CSS of any of
your owned magazines.
oauth2.grant.moderate.magazine_admin.moderators: Add or remove moderators of any
of your owned magazines.
oauth2.grant.moderate.magazine_admin.badges: Create or remove badges from your
owned magazines.
oauth2.grant.moderate.magazine_admin.tags: Create or remove tags from your owned
magazines.
oauth2.grant.moderate.magazine_admin.stats: View the content, vote, and view
stats of your owned magazines.
oauth2.grant.admin.all: Perform any administrative action on your instance.
oauth2.grant.admin.entry.purge: Completely delete any thread from your instance.
oauth2.grant.read.general: Read all content you have access to.
oauth2.grant.write.general: Create or edit any of your threads, posts, or
comments.
oauth2.grant.delete.general: Delete any of your threads, posts, or comments.
oauth2.grant.report.general: Report threads, posts, or comments.
oauth2.grant.vote.general: Upvote, downvote, or boost threads, posts, or
comments.
oauth2.grant.subscribe.general: Subscribe or follow any magazine, domain, or
user, and view the magazines, domains, and users you subscribe to.
oauth2.grant.block.general: Block or unblock any magazine, domain, or user, and
view the magazines, domains, and users you have blocked.
oauth2.grant.domain.all: Subscribe to or block domains, and view the domains you
subscribe to or block.
oauth2.grant.domain.subscribe: Subscribe or unsubscribe to domains and view the
domains you subscribe to.
oauth2.grant.domain.block: Block or unblock domains and view the domains you
have blocked.
oauth2.grant.entry.all: Create, edit, or delete your threads, and vote, boost,
or report any thread.
oauth2.grant.entry.create: Create new threads.
oauth2.grant.entry.edit: Edit your existing threads.
oauth2.grant.entry.delete: Delete your existing threads.
oauth2.grant.entry.vote: Upvote, boost, or downvote any thread.
oauth2.grant.entry.report: Report any thread.
oauth2.grant.entry_comment.all: Create, edit, or delete your comments in
threads, and vote, boost, or report any comment in a thread.
oauth2.grant.entry_comment.create: Create new comments in threads.
oauth2.grant.entry_comment.edit: Edit your existing comments in threads.
oauth2.grant.entry_comment.delete: Delete your existing comments in threads.
oauth2.grant.entry_comment.vote: Upvote, boost, or downvote any comment in a
thread.
oauth2.grant.entry_comment.report: Report any comment in a thread.
oauth2.grant.magazine.all: Subscribe to or block magazines, and view the
magazines you subscribe to or block.
oauth2.grant.magazine.subscribe: Subscribe or unsubscribe to magazines and view
the magazines you subscribe to.
oauth2.grant.magazine.block: Block or unblock magazines and view the magazines
you have blocked.
oauth2.grant.post.all: Create, edit, or delete your microblogs, and vote, boost,
or report any microblog.
oauth2.grant.post.create: Create new posts.
oauth2.grant.post.edit: Edit your existing posts.
oauth2.grant.post.delete: Delete your existing posts.
oauth2.grant.post.vote: Upvote, boost, or downvote any post.
oauth2.grant.post.report: Report any post.
oauth2.grant.post_comment.all: Create, edit, or delete your comments on posts,
and vote, boost, or report any comment on a post.
oauth2.grant.post_comment.create: Create new comments on posts.
oauth2.grant.post_comment.edit: Edit your existing comments on posts.
oauth2.grant.post_comment.delete: Delete your existing comments on posts.
oauth2.grant.post_comment.vote: Upvote, boost, or downvote any comment on a
post.
oauth2.grant.post_comment.report: Report any comment on a post.
oauth2.grant.user.all: Read and edit your profile, messages, or notifications;
Read and edit permissions you've granted other apps; follow or block other
users; view lists of users you follow or block.
oauth2.grant.user.bookmark: Add and remove bookmarks
oauth2.grant.user.bookmark.add: Add bookmarks
oauth2.grant.user.bookmark.remove: Remove bookmarks
oauth2.grant.user.bookmark_list: Read, edit and delete your bookmark lists
oauth2.grant.user.bookmark_list.read: Read your bookmark lists
oauth2.grant.user.bookmark_list.edit: Edit your bookmark lists
oauth2.grant.user.bookmark_list.delete: Delete your bookmark lists
oauth2.grant.user.profile.all: Read and edit your profile.
oauth2.grant.user.profile.read: Read your profile.
oauth2.grant.user.profile.edit: Edit your profile.
oauth2.grant.user.message.all: Read your messages and send messages to other
users.
oauth2.grant.user.message.read: Read your messages.
oauth2.grant.user.message.create: Send messages to other users.
oauth2.grant.user.notification.all: Read and clear your notifications.
oauth2.grant.user.notification.read: Read your notifications, including message
notifications.
oauth2.grant.user.notification.delete: Clear your notifications.
oauth2.grant.user.oauth_clients.all: Read and edit the permissions you have
granted to other OAuth2 applications.
oauth2.grant.user.oauth_clients.read: Read the permissions you have granted to
other OAuth2 applications.
oauth2.grant.user.oauth_clients.edit: Edit the permissions you have granted to
other OAuth2 applications.
oauth2.grant.user.follow: Follow or unfollow users, and read a list of users you
follow.
oauth2.grant.user.block: Block or unblock users, and read a list of users you
block.
oauth2.grant.moderate.all: Perform any moderation action you have permission to
perform in your moderated magazines.
oauth2.grant.moderate.entry.all: Moderate threads in your moderated magazines.
oauth2.grant.moderate.entry.change_language: Change the language of threads in
your moderated magazines.
oauth2.grant.moderate.entry.pin: Pin threads to the top of your moderated
magazines.
oauth2.grant.moderate.entry.lock: Lock threads in your moderated magazines,
so no one can comment on it
oauth2.grant.moderate.entry.set_adult: Mark threads as NSFW in your moderated
magazines.
oauth2.grant.moderate.entry.trash: Trash or restore threads in your moderated
magazines.
oauth2.grant.moderate.entry_comment.all: Moderate comments in threads in your
moderated magazines.
oauth2.grant.moderate.entry_comment.change_language: Change the language of
comments in threads in your moderated magazines.
oauth2.grant.moderate.entry_comment.set_adult: Mark comments in threads as NSFW
in your moderated magazines.
oauth2.grant.moderate.entry_comment.trash: Trash or restore comments in threads
in your moderated magazines.
oauth2.grant.moderate.post.all: Moderate posts in your moderated magazines.
oauth2.grant.moderate.post.change_language: Change the language of posts in your
moderated magazines.
oauth2.grant.moderate.post.lock: Lock microblogs in your moderated magazines,
so no one can comment on it
oauth2.grant.moderate.post.set_adult: Mark posts as NSFW in your moderated
magazines.
oauth2.grant.moderate.post.trash: Trash or restore posts in your moderated
magazines.
oauth2.grant.moderate.post_comment.all: Moderate comments on posts in your
moderated magazines.
oauth2.grant.moderate.post_comment.change_language: Change the language of
comments on posts in your moderated magazines.
oauth2.grant.moderate.post_comment.set_adult: Mark comments on posts as NSFW in
your moderated magazines.
oauth2.grant.moderate.post_comment.trash: Trash or restore comments on posts in
your moderated magazines.
oauth2.grant.moderate.magazine.all: Manage bans, reports, and view trashed items
in your moderated magazines.
oauth2.grant.moderate.magazine.ban.all: Manage banned users in your moderated
magazines.
oauth2.grant.moderate.magazine.ban.read: View banned users in your moderated
magazines.
oauth2.grant.moderate.magazine.ban.create: Ban users in your moderated
magazines.
oauth2.grant.admin.entry_comment.purge: Completely delete any comment in a
thread from your instance.
oauth2.grant.admin.post.purge: Completely delete any post from your instance.
oauth2.grant.admin.post_comment.purge: Completely delete any comment on a post
from your instance.
oauth2.grant.admin.magazine.all: Move threads between or completely delete
magazines on your instance.
oauth2.grant.admin.magazine.move_entry: Move threads between magazines on your
instance.
oauth2.grant.admin.magazine.purge: Completely delete magazines on your instance.
oauth2.grant.admin.user.all: Ban, verify, or completely delete users on your
instance.
oauth2.grant.admin.user.ban: Ban or unban users from your instance.
oauth2.grant.admin.user.verify: Verify users on your instance.
oauth2.grant.admin.user.delete: Delete users from your instance.
oauth2.grant.admin.user.purge: Completely delete users from your instance.
oauth2.grant.admin.instance.all: View and update instance settings or
information.
oauth2.grant.admin.instance.stats: View your instance's stats.
oauth2.grant.admin.instance.settings.all: View or update settings on your
instance.
oauth2.grant.admin.instance.settings.read: View settings on your instance.
oauth2.grant.admin.instance.settings.edit: Update settings on your instance.
oauth2.grant.admin.instance.information.edit: Update the About, FAQ, Contact,
Terms of Service, and Privacy Policy pages on your instance.
oauth2.grant.admin.federation.all: View and update currently defederated
instances.
oauth2.grant.admin.federation.read: View the list of defederated instances.
oauth2.grant.admin.federation.update: Add or remove instances to or from the
list of defederated instances.
oauth2.grant.admin.oauth_clients.all: View or revoke OAuth2 clients that exist
on your instance.
oauth2.grant.admin.oauth_clients.read: View the OAuth2 clients that exist on
your instance, and their usage stats.
oauth2.grant.admin.oauth_clients.revoke: Revoke access to OAuth2 clients on your
instance.
last_active: Last Active
flash_post_pin_success: The post has been successfully pinned.
flash_post_unpin_success: The post has been successfully unpinned.
comment_reply_position_help: Display the comment reply form either at the top or
bottom of the page. When 'infinite scroll' is enabled the position will always
appear at the top.
show_avatars_on_comments: Show Comment Avatars
single_settings: Single
update_comment: Update comment
show_avatars_on_comments_help: Display/hide user avatars when viewing comments
on a single thread or post.
comment_reply_position: Comment reply position
magazine_theme_appearance_custom_css: Custom CSS that will apply when viewing
content within your magazine.
magazine_theme_appearance_icon: Custom icon for the magazine.
magazine_theme_appearance_banner: Custom banner for the magazine. It is
displayed above all threads and should be in a wide aspect ratio (5:1, or
1500px * 300px).
magazine_theme_appearance_background_image: Custom background image that will be
applied when viewing content within your magazine.
moderation.report.approve_report_title: Approve Report
moderation.report.reject_report_title: Reject Report
moderation.report.ban_user_description: Do you want to ban the user (%username%)
who created this content from this magazine?
moderation.report.approve_report_confirmation: Are you sure that you want to
approve this report?
subject_reported_exists: This content has already been reported.
moderation.report.ban_user_title: Ban User
moderation.report.reject_report_confirmation: Are you sure that you want to
reject this report?
oauth2.grant.moderate.post.pin: Pin posts to the top of your moderated
magazines.
delete_content: Delete content
purge_content: Purge content
delete_content_desc: Delete the user's content while leaving the responses of
other users in the created threads, posts and comments.
purge_content_desc: Completely purge the user's content, including deleting the
responses of other users in created threads, posts and comments.
delete_account_desc: Delete the account, including the responses of other users
in created threads, posts and comments.
schedule_delete_account: Schedule Deletion
schedule_delete_account_desc: Schedule the deletion of this account in 30 days.
This will hide the user and their content as well as prevent the user from
logging in.
remove_schedule_delete_account: Remove Scheduled Deletion
remove_schedule_delete_account_desc: Remove the scheduled deletion. All the
content will be available again and the user will be able to login.
two_factor_authentication: Two-factor authentication
two_factor_backup: Two-factor authentication backup codes
2fa.authentication_code.label: Authentication Code
2fa.verify: Verify
2fa.code_invalid: The authentication code is not valid
2fa.setup_error: Error enabling 2FA for account
2fa.enable: Setup two-factor authentication
2fa.disable: Disable two-factor authentication
2fa.backup: Your two-factor backup codes
2fa.backup-create.help: You can create new backup authentication codes; doing so
will invalidate existing codes.
2fa.backup-create.label: Create new backup authentication codes
2fa.remove: Remove 2FA
2fa.add: Add to my account
2fa.verify_authentication_code.label: Enter a two-factor code to verify setup
2fa.qr_code_img.alt: A QR code that allows the setup of two-factor
authentication for your account
2fa.qr_code_link.title: Visiting this link may allow your platform to register
this two-factor authentication
2fa.user_active_tfa.title: User has active 2FA
2fa.available_apps: Use a two-factor authentication app such as
%google_authenticator%, %aegis% (Android) or %raivo% (iOS) to scan the
QR-code.
2fa.backup_codes.help: You can use these codes when you don't have your
two-factor authentication device or app. You will not be shown them
again and will be able to use each of them only
once .
2fa.backup_codes.recommendation: It is recommended that you keep a copy of them
in a safe place.
2fa.manual_code_hint: If you cannot scan the QR code, enter the secret manually
cancel: Cancel
password_and_2fa: Password & 2FA
flash_account_settings_changed: Your account settings have been successfully
changed. You will need to login again.
show_subscriptions: Show subscriptions
subscription_sort: Sort
alphabetically: Alphabetically
subscriptions_in_own_sidebar: In separate sidebar
sidebars_same_side: Sidebars on the same side
subscription_sidebar_pop_out_right: Move to separate sidebar on the right
subscription_sidebar_pop_out_left: Move to separate sidebar on the left
subscription_sidebar_pop_in: Move subscriptions to the inline panel
subscription_panel_large: Large panel
subscription_header: Subscribed Magazines
close: Close
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_thread_ref_image_not_found: The image referenced by 'imageHash' could not
be found.
flash_image_download_too_large_error: Image could not be created, it is too big
(max size %bytes%)
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.
flash_post_new_error: Post could not be created. Something went wrong.
flash_magazine_theme_changed_success: Successfully updated the magazine
appearance.
flash_magazine_theme_changed_error: Failed to update the magazine appearance.
flash_comment_new_success: Comment has been successfully created.
flash_comment_edit_success: Comment has been successfully updated.
flash_comment_new_error: Failed to create comment. Something went wrong.
flash_comment_edit_error: Failed to edit comment. Something went wrong.
flash_user_settings_general_success: User settings successfully saved.
flash_user_settings_general_error: Failed to save user settings.
flash_user_edit_profile_error: Failed to save profile settings.
flash_user_edit_profile_success: User profile settings successfully saved.
flash_user_edit_email_error: Failed to change email.
flash_user_edit_password_error: Failed to change password.
flash_thread_edit_error: Failed to edit thread. Something went wrong.
flash_post_edit_error: Failed to edit post. Something went wrong.
flash_post_edit_success: Post has been successfully edited.
page_width: Page width
page_width_max: Max
page_width_auto: Auto
page_width_fixed: Fixed
filter_labels: Filter Labels
auto: Auto
open_url_to_fediverse: Open original URL
change_my_avatar: Change my avatar
change_my_cover: Change my cover
edit_my_profile: Edit my profile
account_settings_changed: Your account settings have been successfully changed.
You will need to login again.
magazine_deletion: Magazine deletion
delete_magazine: Delete magazine
restore_magazine: Restore magazine
purge_magazine: Purge magazine
magazine_is_deleted: Magazine is deleted. You can restore it within 30 days.
suspend_account: Suspend account
unsuspend_account: Unsuspend account
account_suspended: The account has been suspended.
account_unsuspended: The account has been unsuspended.
deletion: Deletion
user_suspend_desc: Suspending your account hides your content on the instance,
but doesn't permanently remove it, and you can restore it at any time.
account_banned: The account has been banned.
account_unbanned: The account has been unbanned.
account_is_suspended: User account is suspended.
remove_following: Remove following
remove_subscriptions: Remove subscriptions
apply_for_moderator: Apply for moderator
request_magazine_ownership: Request magazine ownership
cancel_request: Cancel request
abandoned: Abandoned
ownership_requests: Ownership requests
accept: Accept
moderator_requests: Mod requests
action: Action
user_badge_op: OP
user_badge_admin: Admin
user_badge_global_moderator: Global Mod
user_badge_moderator: Mod
user_badge_bot: Bot
announcement: Announcement
keywords: Keywords
deleted_by_moderator: Thread, post or comment was deleted by the moderator
deleted_by_author: Thread, post or comment was deleted by the author
sensitive_warning: Sensitive content
sensitive_toggle: Toggle visibility of sensitive content
sensitive_show: Click to show
sensitive_hide: Click to hide
details: Details
spoiler: Spoiler
all_time: All time
show: Show
hide: Hide
edited: edited
sso_registrations_enabled: SSO registrations enabled
sso_registrations_enabled.error: New account registrations with third-party
identity managers are currently disabled.
sso_only_mode: Restrict login and registration to SSO methods only
related_entry: Related
restrict_magazine_creation: Restrict local magazine creation to admins and
global mods
sso_show_first: Show SSO first on login and registration pages
continue_with: Continue with
reported_user: Reported user
reporting_user: Reporting user
reported: reported
report_subject: Subject
own_report_rejected: Your report was rejected
own_report_accepted: Your report was accepted
own_content_reported_accepted: A report of your content was accepted.
report_accepted: A report was accepted
open_report: Open report
cake_day: Cake day
someone: Someone
back: Back
magazine_log_mod_added: has added a moderator
magazine_log_mod_removed: has removed a moderator
magazine_log_entry_pinned: pinned entry
magazine_log_entry_unpinned: removed pinned entry
last_updated: Last updated
and: and
direct_message: Direct message
manually_approves_followers: Manually approves followers
register_push_notifications_button: Register For Push Notifications
unregister_push_notifications_button: Remove Push Registration
test_push_notifications_button: Test Push Notifications
test_push_message: Hello World!
notification_title_new_comment: New comment
notification_title_removed_comment: A comment was removed
notification_title_edited_comment: A comment was edited
notification_title_mention: You were mentioned
notification_title_new_reply: New Reply
notification_title_new_thread: New thread
notification_title_removed_thread: A thread was removed
notification_title_edited_thread: A thread was edited
notification_title_ban: You were banned
notification_title_message: New direct message
notification_title_new_post: New Post
notification_title_removed_post: A post was removed
notification_title_edited_post: A post was edited
notification_title_new_signup: A new user registered
notification_body_new_signup: The user %u% registered.
notification_body2_new_signup_approval: You need to approve the request before
they can log in
show_related_magazines: Show random magazines
show_related_entries: Show random threads
show_related_posts: Show random posts
show_active_users: Show active users
notification_title_new_report: A new report was created
magazine_posting_restricted_to_mods_warning: Only mods can create threads in
this magazine
flash_posting_restricted_error: Creating threads is restricted to mods in this
magazine and you are not one
server_software: Server software
version: Version
last_successful_deliver: Last successful delivery
last_successful_receive: Last successful receive
last_failed_contact: Last failed contact
magazine_posting_restricted_to_mods: Restrict thread creation to moderators
new_user_description: This user is new (active for less than %days% days)
new_magazine_description: This magazine is new (active for less than %days%
days)
admin_users_active: Active
admin_users_inactive: Inactive
admin_users_suspended: Suspended
admin_users_banned: Banned
user_verify: Activate account
max_image_size: Maximum file size
comment_not_found: Comment not found
bookmark_add_to_list: Add bookmark to %list%
bookmark_remove_from_list: Remove bookmark from %list%
bookmark_remove_all: Remove all bookmarks
bookmark_add_to_default_list: Add bookmark to default list
bookmark_lists: Bookmark Lists
bookmarks: Bookmarks
bookmarks_list: Bookmarks in %list%
count: Count
is_default: Is Default
bookmark_list_is_default: Is default list
bookmark_list_make_default: Make Default
bookmark_list_create: Create
bookmark_list_create_placeholder: type name...
bookmark_list_create_label: List name
bookmarks_list_edit: Edit bookmark list
bookmark_list_edit: Edit
bookmark_list_selected_list: Selected list
table_of_contents: Table of contents
search_type_all: Everything
search_type_entry: Threads
search_type_post: Microblogs
search_type_magazine: Magazines
search_type_user: Users
search_type_actors: Magazines + Users
search_type_content: Threads + Microblogs
select_user: Choose a user
new_users_need_approval: New users have to be approved by an admin before they
can log in.
signup_requests: Signup requests
application_text: Explain why you want to join
signup_requests_header: Signup Requests
signup_requests_paragraph: These users would like to join your server. They
cannot log in until you've approved their signup request.
flash_application_info: An admin needs to approve your account before you can
log in. You will receive an email once your signup request has been processed.
email_application_approved_title: Your signup request has been approved
email_application_approved_body: Your signup request was approved by the server
admin. You can now log into the server at %siteName% .
email_application_rejected_title: Your signup request has been rejected
email_application_rejected_body: Thank you for your interest, but we regret to
inform you that your signup request has been declined.
email_application_pending: Your account requires admin approval before you can
log in.
email_verification_pending: You have to verify your email address before you can
log in.
show_magazine_domains: Show magazine domains
show_user_domains: Show user domains
answered: answered
by: by
front_default_sort: Frontpage default sort
comment_default_sort: Comment default sort
open_signup_request: Open signup request
image_lightbox_in_list: Thread thumbnails opens full screen
compact_view_help: A compact view with less margins, where the media is moved to
the right side.
show_users_avatars_help: Display the user avatar image.
show_magazines_icons_help: Display the magazine icon.
show_thumbnails_help: Show the thumbnail images.
image_lightbox_in_list_help: When checked, clicking the thumbnail shows a modal
image box window. When unchecked, clicking the thumbnail will open the thread.
show_new_icons: Show new icons
show_new_icons_help: Show icon for new magazine/user (30 days old or newer)
magazine_instance_defederated_info: The instance of this magazine is
defederated. The magazine will therefore not receive updates.
user_instance_defederated_info: The instance of this user is defederated.
flash_thread_instance_banned: The instance of this magazine is banned.
show_rich_mention: Rich mentions
show_rich_mention_help: Render a user component when a user is mentioned. This
will include their display name and profile picture.
show_rich_mention_magazine: Rich magazine mentions
show_rich_mention_magazine_help: Render a magazine component when a magazine is
mentioned. This will include their display name and icon.
show_rich_ap_link: Rich AP links
show_rich_ap_link_help: Render an inline component when other ActivityPub
content is linked to.
attitude: Attitude
type_search_magazine: Limit search to magazine...
type_search_user: Limit search to author...
modlog_type_entry_deleted: Thread deleted
modlog_type_entry_restored: Thread restored
modlog_type_entry_comment_deleted: Thread comment deleted
modlog_type_entry_comment_restored: Thread comment restored
modlog_type_entry_pinned: Thread pinned
modlog_type_entry_unpinned: Thread unpinned
modlog_type_post_deleted: Microblog deleted
modlog_type_post_restored: Microblog restored
modlog_type_post_comment_deleted: Microblog reply deleted
modlog_type_post_comment_restored: Microblog reply restored
modlog_type_ban: User banned from magazine
modlog_type_moderator_add: Magazine moderator added
modlog_type_moderator_remove: Magazine moderator removed
everyone: Everyone
nobody: Nobody
followers_only: Followers only
direct_message_setting_label: Who can send you a direct message
show_boost_following_label: Show boosted content in Microblog and Combined view
show_boost_following_help: If this is enabled, threads, posts and comments boosted by you or users you follow
will show up in the Combined view of your subscriptions and Microblog view.
delete_magazine_icon: Delete magazine icon
flash_magazine_theme_icon_detached_success: Magazine icon deleted successfully
delete_magazine_banner: Delete magazine banner
flash_magazine_theme_banner_detached_success: Magazine banner deleted
successfully
federation_uses_allowlist: Use allowlist for federation
defederating_instance: Defederating instance %i
their_user_follows: Amount of users from their instance following users on our
instance
our_user_follows: Amount of users from our instance following users on their
instance
their_magazine_subscriptions: Amount of users from their instance subscribed to
magazines on our instance
our_magazine_subscriptions: Amount of users on our instance subscribed to
magazines from their instance
confirm_defederation: Confirm defederation
flash_error_defederation_must_confirm: You have to confirm the defederation
allowed_instances: Allowed instances
btn_deny: Deny
btn_allow: Allow
ban_instance: Ban instance
allow_instance: Allow instance
federation_page_use_allowlist_help: If an allow list is used, this instance will
only federate with the explicitly allowed instances. Otherwise this instance
will federate with every instance, except those that are banned.
you_have_been_banned_from_magazine: You have been banned from magazine %m.
you_have_been_banned_from_magazine_permanently: You have been permanently banned from magazine %m.
you_are_no_longer_banned_from_magazine: You are no longer banned from magazine %m.
front_default_content: Frontpage default view
default_content_default: Server default (Threads)
default_content_combined: Threads + Microblog
default_content_threads: Threads
default_content_microblog: Microblog
combined: Combined
sidebar_sections_random_local_only: Restrict "Random Threads/Posts" sidebar sections to local only
sidebar_sections_users_local_only: Restrict "Active people" sidebar section to local only
random_local_only_performance_warning: Enabling "Random local only" may cause SQL performance impact.
discoverable: Discoverable
user_discoverable_help: If this is enabled, your profile, threads, microblogs and comments can be found
through search and the random panels. Your profile might also appear in the active user panel and on the people page.
If this is disabled, your posts will still be visible to other users, but they will not show up in the all feed.
magazine_discoverable_help: If this is enabled, this magazine and threads, microblogs and comments of this magazine
can be found through search and the random panels.
If this is disabled the magazine will still appear in the magazine list, but the threads and microblogs will not
appear in the all feed.
flash_thread_lock_success: Thread locked successfully
flash_thread_unlock_success: Thread unlocked successfully
flash_post_lock_success: Microblog locked successfully
flash_post_unlock_success: Microblog unlocked successfully
lock: Lock
unlock: Unlock
comments_locked: The comments are locked.
magazine_log_entry_locked: locked the comments of
magazine_log_entry_unlocked: unlocked the comments of
modlog_type_entry_lock: Thread locked
modlog_type_entry_unlock: Thread unlocked
modlog_type_post_lock: Microblog locked
modlog_type_post_unlock: Microblog unlocked
contentnotification.muted: Mute | get no notifications
contentnotification.default: Default | get notifications according to your default settings
contentnotification.loud: Loud | get all notifications
indexable_by_search_engines: Indexable by search engines
user_indexable_by_search_engines_help: If this setting is false, search engines are advised to not index any of your threads
and microblogs, however your comments are not affected by this and bad actors might ignore it. This setting is also
federated to other servers.
magazine_indexable_by_search_engines_help: If this setting is false, search engines are advised to not index any of the
threads and microblogs in this magazines. That includes the landing page and all comment pages. This setting is also
federated to other servers.
magazine_name_as_tag: Use the magazine name as a tag
magazine_name_as_tag_help: The tags of a magazine are used to match microblog posts to this magazine.
For example if the name is "fediverse" and the magazine tags contain "fediverse", then every microblog post
containing "#fediverse" will be put in this magazine.
magazine_rules_deprecated: the rules field is deprecated and will be removed in the future.
Please put your rules in the description box.
monitoring: Monitoring
monitoring_queries: '{0}SQL Queries|{1}SQL Query|]1,Inf[ SQL Queries'
monitoring_duration_min: min
monitoring_duration_mean: mean
monitoring_duration_max: max
monitoring_query_count: count
monitoring_query_total: total
monitoring_duration: Duration
monitoring_dont_group_similar: don't group similar queries
monitoring_group_similar: group similar queries
monitoring_http_method: HTTP Method
monitoring_url: URL
monitoring_request_successful: Successful
monitoring_user_type: User Type
monitoring_path: Route/Message class
monitoring_handler: Controller/Transport
monitoring_started: Started
monitoring_twig_renders: Twig Renders
monitoring_curl_requests: Curl Requests
monitoring_route_overview: Spend execution time summed per route
monitoring_route_overview_description: This graph shows the summed milliseconds spent grouped by the route/message
being computed
monitoring_duration_overall: Other
monitoring_duration_query: Query
monitoring_duration_twig_render: Twit Render
monitoring_duration_curl_request: Curl Request
monitoring_duration_sending_response: Sending Response
monitoring_dont_format_query: don't format query
monitoring_format_query: format query
monitoring_dont_show_parameters: don't show parameters
monitoring_show_parameters: show parameters
monitoring_execution_type: Execution Type
monitoring_request: HTTP Request
monitoring_messenger: Messenger
monitoring_anonymous: Anonymous
monitoring_user: User
monitoring_activity_pub: ActivityPub
monitoring_ajax: AJAX
monitoring_created_from: Started after
monitoring_created_to: Started before
monitoring_duration_minimum: Minimum duration
monitoring_submit: Filter
monitoring_has_exception: Has exception
monitoring_chart_ordering: Chart ordering
monitoring_total_duration: Total duration
monitoring_mean_duration: Mean duration
monitoring_twig_compare_to_total: Compare to total duration
monitoring_twig_compare_to_parent: Compare to parent duration
monitoring_disabled: Monitoring is disabled.
monitoring_queries_enabled_persisted: Query monitoring is enabled.
monitoring_queries_enabled_not_persisted: Query monitoring is enabled, but only the execution time.
monitoring_queries_disabled: Query monitoring is disabled.
monitoring_twig_renders_enabled_persisted: Twig render monitoring is enabled.
monitoring_twig_renders_enabled_not_persisted: Twig rendering is enabled, but only the execution time.
monitoring_twig_renders_disabled: Twig render monitoring is disabled.
monitoring_curl_requests_enabled_persisted: Curl request monitoring is enabled.
monitoring_curl_requests_enabled_not_persisted: Curl request monitoring is enabled, but only the execution time.
monitoring_curl_requests_disabled: Curl request monitoring is disabled.
reached_end: You've reached the end
first_page: First page
next_page: Next page
previous_page: Previous page
filter_list_create: Create Filter List
filter_lists: Filter lists
filter_lists_where_to_filter: Where to apply the filter
filter_lists_filter_words: Filtered words
expiration_date: Expiration date
filter_lists_filter_location: Active in
filter_lists_word_exact_match: Exact match
filter_lists_word_exact_match_help: If exact match is true, then the search will be case sensitive
feeds: Feeds
filter_lists_feeds_help: Filter words in threads, microblogs and comments in feeds, like /all, /sub, magazine feeds, etc.
filter_lists_comments_help: Filter words while viewing a thread or microblog in the comment tree.
filter_lists_profile_help: Filter words while viewing a users' profile in their content.
expired: Expired
================================================
FILE: translations/messages.eo.yaml
================================================
type.article: Fadeno
type.photo: Foto
type.video: Filmeto
type.smart_contract: Inteligenta kontrakto
type.magazine: Revuo
people: Homoj
events: Eventoj
magazine: Revuo
magazines: Revuoj
search: Serĉi
add: Aldoni
select_channel: Elektu kanalon
login: Ensaluti
top: Supro
hot: Furora
active: Aktiva
newest: Plej nova
commented: Komentis
change_view: Ŝanĝi aspekton
filter_by_time: Filtri laŭ tempo
filter_by_type: Filtri laŭ tipo
favourites: Porvoĉdonoj
favourite: Igi plej ŝatata
added: Aldonis
up_votes: Akceloj
down_votes: Reduktoj
created_at: Kreita
owner: Proprulo
subscribers: Abonantoj
replies: Respondoj
empty: Malplena
unsubscribe: Malaboni
remember_me: Memoru min
unfollow: Malsekvi
terms: Servadokondiĉoj
privacy_policy: Privateca politiko
add_new_article: Aldoni novan fadenon
change_theme: Ŝanĝi etoson
useful: Utila
help: Helpo
cards_view: Kartoj aspekto
3h: 3h
6h: 6h
12h: 12h
1d: 1t
1w: 1s
1m: 1m
articles: Fadenoj
photos: Fotoj
videos: Filmetoj
share: Kunhavigi
copy_url: Kopii Mbin URL
copy_url_to_fediverse: Kopii originala URL
share_on_fediverse: Kunhavigi en Fediverso
are_you_sure: Ĉu vi certas?
type.link: Ligilo
thread: Fadeno
threads: Fadenoj
microblog: Mikroblogo
oldest: Plej malnova
more: Pli
avatar: Avataro
no_comments: Sen komentoj
online: Reta
comments: Komentoj
posts: Afiŝoj
moderators: Moderigantoj
federated_magazine_info: Ĉi tiu revuo estas el federaciita servilo kaj povas
esti nekompleta.
federated_user_info: Ĉi tiu profilo estas el federaciita servilo kaj povas esti
nekompleta.
go_to_original_instance: Rigardi ĝin sur la fora nodo
reset_check_email_desc: Se jam estas konto asociita kun via retpoŝtadreso, vi
baldaŭ ricevu retmesaĝon enhavantan ligilon, kiun vi povas uzi por reagordi
vian pasvorton. Ĉi tiu ligilo eksvalidiĝos en %expire%.
login_or_email: Uzantnomo aŭ retpoŝtadreso
agree_terms: Konsentu al %terms_link_start%Kondiĉoj%terms_link_end% kaj
%policy_link_start%Privateca Politiko%policy_link_end%
reset_check_email_desc2: Se vi ne ricevas retmesaĝon, bonvolu kontroli vian
spam-dosierujon.
check_email: Kontrolu vian retpoŝton
table_view: Tablo aspekto
theme: Etoso
1y: 1j
comment: Komento
post: Afiŝo
links: Ligiloj
all: Ĉiuj
report: Raporti
infinite_scroll: Senfina rulumado
edit: Redakti
add_post: Aldoni afiŝon
add_media: Aldoni aŭdvidaĵon
enter_your_post: Enigu vian afiŝon
activity: Aktiveco
password: Pasvorto
already_have_account: Ĉu vi jam havas konton?
reset_password: Reagordi pasvorton
to: al
username: Uzantnomo
email: Retpoŝtadreso
repeat_password: Ripetu pasvorton
stats: Statistikoj
fediverse: Fediverso
add_new_link: Aldoni novan ligilon
add_new_photo: Aldoni novan foton
add_new_video: Aldoni novan filmeton
contact: Kontakto
faq: Oftaj demandoj
rss: RSS
random_magazines: Hazardaj revuoj
kbin_promo_title: Krei vian propran nodon
comments_count: '{0}Komentoj|{1}Komento|]1,Inf[ Komentoj'
add_comment: Aldoni komenton
markdown_howto: Kiel la redaktilo funkcias?
enter_your_comment: Enigu vian komenton
cover: Kovrilo
related_posts: Rilataj afiŝoj
random_posts: Hazardaj afiŝoj
subscribe: Aboni
follow: Sekvi
reply: Respondi
dont_have_account: Ĉu vi ne havas konton?
you_cant_login: Ĉu vi forgesis vian pasvorton?
register: Registriĝi
show_more: Montri pli
in: en
about_instance: Pri
all_magazines: Ĉiuj revuoj
create_new_magazine: Krei novan revuon
add_new_post: Aldoni novan afiŝon
tokyo_night: Tokia Nokto
captcha_enabled: «Captcha» ebligita
up_vote: Akceli
down_vote: Redukti
email_confirm_content: 'Ĉu vi pretas aktivigi vian Mbin-konton? Alklaku la suban ligilon:'
email_verify: Konfirmu la retpoŝtadreson
select_magazine: Elektu revuon
add_new: Aldoni novan
url: URL
image: Bildo
title: Titolo
body: Korpo
tags: Etikedoj
badges: Insignoj
is_adult: 18+ / NSPL
domain: Domajno
name: Nomo
description: Priskribo
following: Sekvado
subscriptions: Abonoj
overview: Superrigardo
cards: Kartoj
columns: Kolumnoj
user: Uzanto
joined: Aliĝis
people_federated: Federaciita
related_tags: Rilataj etikedoj
go_to_content: Iri al enhavo
go_to_search: Iri al serĉi
subscribed: Abonita
logout: Elsaluti
classic_view: Klasika aspekto
compact_view: Kompacta aspekto
chat_view: Babilo aspekto
tree_view: Arba aspekto
moderate: Kontroli
reason: Kialo
delete: Forigi
edit_post: Redakti afiŝon
settings: Agordoj
general: Ĝenerala
reports: Raportoj
messages: Mesaĝojn
appearance: Aspekto
homepage: Hejmpaĝo
hide_adult: Kaŝi NSPL-enhavon
privacy: Privateco
show_profile_subscriptions: Montri revuon abonojn
show_profile_followings: Montri sekvajn uzantojn
notify_on_new_entry_comment_reply: Respondoj al miaj komentoj en iuj fadenoj
notify_on_new_post_reply: Respondoj je ajna nivelo al afiŝoj, kiujn mi verkis
notify_on_new_post_comment_reply: Respondoj al miaj komentoj pri iuj afiŝoj
notify_on_new_entry: Novaj fadenoj (ligiloj aŭ artikoloj) en iu ajn revuo, al
kiu mi estas abonita
save: Konservi
about: Pri
old_email: Nuna retpoŝtadreso
new_email: Nova retpoŝtadreso
new_email_repeat: Konfirmu novan retpoŝtadreson
current_password: Nuna pasvorto
new_password: Nova pasvorto
new_password_repeat: Konfirmu novan pasvorton
expand: Vastigi
collapse: Kolapsi
domains: Domajnoj
error: Eraro
votes: Voĉdonoj
dark: Malhela
font_size: Tipara grando
size: Grando
boosts: Akceloj
show_users_avatars: Montri avatarojn de uzantoj
yes: Jes
no: Ne
show_magazines_icons: Montri ikonojn de revuoj
show_thumbnails: Montri bildetojn
rounded_edges: Rondigitaj randoj
restored_thread_by: restaŭris fadenon de
removed_post_by: forigis afiŝon de
restored_comment_by: restaŭris komenton de
restored_post_by: restaŭris afiŝon de
read_all: Legi ĉion
show_all: Montri ĉion
set_magazines_bar: Revuoj breto
set_magazines_bar_desc: aldonu la revuonomojn post la komo
flash_thread_new_success: La fadeno estis kreita sukcese kaj nun videblas por
aliaj uzantoj.
flash_thread_edit_success: La fadeno estas sukcese redaktita.
flash_thread_delete_success: La fadeno estas sukcese forigita.
flash_thread_pin_success: La fadeno estas sukcese alpinglita.
flash_thread_unpin_success: La fadeno estas sukcese depinglita.
flash_magazine_edit_success: La revuo estas sukcese redaktita.
rules: Reguloj
followers: Sekvantoj
oc: OE
image_alt: Bildo alternativa teksto
email_confirm_title: Konfirmu vian retpoŝtadreson.
email_confirm_header: Saluton! Konfirmu vian retpoŝtadreson.
email_confirm_expire: Bonvolu noti, ke la ligilo eksvalidiĝos post horo.
removed_comment_by: forigis komenton de
removed_thread_by: forigis fadenon de
too_many_requests: Limo superita, bonvolu provi denove poste.
set_magazines_bar_empty_desc: se la kampo estas malplena, aktivaj revuoj estas
montrataj sur la breto.
edit_comment: Konservi ŝanĝojn
profile: Profilo
mod_log: Kontrol-protokolo
flash_register_success: Bonvenon surŝipe! Via konto nun estas registrita. Unu
fina paŝo - kontrolu vian enirkeston por aktiviga ligilo, kiu vivigos vian
konton.
people_local: Loka
reputation_points: Reputaciopoentoj
flash_magazine_new_success: La revuo estas sukcese kreita. Vi nun povas aldoni
novan enhavon aŭ esplori la administran panelon de la revuo.
go_to_filters: Iri al filtriloj
notifications: Sciigoj
featured_magazines: Elstaraj revuoj
notify_on_new_entry_reply: Komentoj je ajna nivelo en fadenoj kiujn mi verkis
notify_on_new_posts: Novaj afiŝoj en iu ajn revuo, al kiu mi estas abonita
change_email: Ŝanĝi retpoŝtadreson
change_password: Ŝanĝi pasvorton
light: Hela
try_again: Provu denove
blocked: Blokita
he_banned: forbari
he_unbanned: malforbari
banned: Forbaris vin
deleted: Forigita de la aŭtoro
mentioned_you: Menciis vin
send_message: Sendi rektan mesaĝon
message: Mesaĝo
from_url: El URL
users: Uzantoj
content: Enhavo
week: Semajno
weeks: Semajnoj
months: Monatoj
year: Jaro
pages: Paĝoj
FAQ: Oftaj demandoj
Your account is not active: Via konto ne estas aktiva.
related_entries: Rilataj fadenoj
related_magazines: Rilataj revuoj
banned_instances: Forbaritaj nodoj
wrote_message: Skribis mesaĝon
instances: Nodoj
upload_file: Alŝuti dosieron
ban: Forbari
change_language: 'Ŝanĝi lingvon'
article: Fadeno
reputation: Reputacio
month: Monato
instance: Nodo
Password is invalid: Pasvorto nevalidas.
Your account has been banned: Via konto estis forbarita.
send: Sendi
random_entries: Hazardaj fadenoj
delete_account: Forigi konton
ban_account: Forbari konton
unban_account: Malforbari konton
moderated: Moderigita
solarized_light: Hele sunlumigita
solarized_dark: Malhele Sunlumigita
mod_log_alert: AVERTO - La kontrol-protokolo povus enhavi malagrablan aŭ
afliktan enhavon, kiu estis forigita de moderigantoj. Bonvolu esti singarda.
added_new_thread: Aldonis novan fadenon
boost: Diskonigi
edited_thread: Redaktis fadenon
added_new_comment: Aldonis novan komenton
edited_comment: Redaktis komenton
mod_deleted_your_comment: Moderiganto forigis vian komenton
edited_post: Redaktis afiŝon
mod_remove_your_post: Moderiganto forigis vian afiŝon
added_new_reply: Aldonis novan respondon
mod_remove_your_thread: Moderiganto forigis vian fadenon
replied_to_your_comment: Respondis al via komento
added_new_post: Aldonis novan afiŝon
removed: Forigita de moderiganto
ban_expired: Forbaro eksvalidiĝis
purge: Viŝi
add_moderator: Aldoni moderiganton
sticky_navbar: Gluita naviga breto
show_top_bar: Montri supran breton
eng: ENG
subject_reported: La enhavo estis raportita.
left: Maldekstre
right: Dekstre
approve: Aprobi
approved: Aprobita
trash: Rubujo
federation: Federacio
filters: Filtriloj
sidebar_position: Flanka kolumno pozicio
status: Stato
off: Malŝalta
magazine_panel: Revua panelo
reject: Malakcepti
rejected: Malakceptita
add_badge: Aldoni insignon
perm: Permanenta
expired_at: Eksvalidiĝis je
add_ban: Aldoni forbaron
icon: Ikono
done: Farita
pin: Alpingli
unpin: Depingli
change_magazine: Ŝanĝi revuon
change: Ŝanĝi
pinned: Alpinglita
preview: Antaŭrigardi
note: Noton
writing: Skribado
federated: Federaciita
local: Loka
admin_panel: Administra panelo
dashboard: Panelo
contact_email: Kontakta retpoŝtadreso
meta: Meta
federation_enabled: Federado ebligita
registration_disabled: Registrado malebligita
restore: Restarigi
add_mentions_posts: Aldoni mencioetikedojn en afiŝoj
firstname: Persona nomo
active_users: Aktivaj homoj
sidebar: Flanka kolumno
auto_preview: Aŭtomata aŭdvidaĵa antaŭvido
dynamic_lists: Dinamikaj listoj
kbin_intro_title: Esplori la Fediverso
kbin_promo_desc: '%link_start%Kloni la deponejon%link_end% kaj programi la fediverson'
header_logo: Kapa emblemo
return: Reveni
browsing_one_thread: Vi nur foliumas unu fadenon en la diskuto! Ĉiuj komentoj
disponeblas en la afiŝa paĝo.
report_issue: Raporti problemon
mercure_enabled: Mercure ebligita
preferred_languages: Filtri lingvoj de fadenoj kaj afiŝoj
type_search_term: Tajpu serĉterminon
registrations_enabled: Registrado ebligita
add_mentions_entries: Aldoni mencioetikedojn en fadenoj
purge_account: Viŝi konton
magazine_panel_tags_info: Provizu nur se vi volas, ke enhavo de la fediverso
estu inkluzivita en ĉi tiu revuo bazita sur etikedoj
on: Ŝalta
bans: Forbaroj
created: Kreita
expires: Eksvalidiĝas
kbin_intro_desc: estas malcentra platformo por enhavo-kolektado kaj
mikroblogado, kiu funkcias ene de la Fediversa reto.
infinite_scroll_help: Aŭtomate ŝarĝi pli da enhavo kiam vi atingas la malsupron
de la paĝo.
sticky_navbar_help: La navigadbreto algluiĝos al la supro de la paĝo kiam vi
rulumas malsupren.
auto_preview_help: Montri la antaŭrigardojn de la aŭdvidaĵaj (fotaj, filmetaj)
en pli granda grandeco sub la enhavo.
reload_to_apply: Reŝargi paĝon por apliki ŝanĝojn
filter.origin.label: Elekti originon
filter.fields.label: Elekti kiujn kampojn por serĉi
filter.adult.label: Elekti ĉu montri NSPL
filter.adult.hide: Kaŝi NSPL
filter.adult.only: Nur NSPL
filter.fields.only_names: Nur nomoj
filter.fields.names_and_descriptions: Nomoj kaj priskriboj
kbin_bot: Mbin Agento
filter.adult.show: Montri NSPL
local_and_federated: Loka kaj federaciita
bot_body_content: "Bonvenon al la Mbin Agento! Ĉi tiu agento ludas decidan rolon en
ebligado de ActivityPub-funkcio ene de Mbin. Ĝi certigas, ke Mbin povas komuniki
kaj federacii kun aliaj nodoj en la fediverso.\n\nActivityPub estas malferma norma
protokolo, kiu permesas al malcentraj sociaj retaj platformoj komuniki kaj interagi
unu kun la alia. Ĝi ebligas al uzantoj en malsamaj nodoj (serviloj) sekvi, interagi
kun, kaj kundividi enhavon tra la federacia socia reto konata kiel la fediverso.
Ĝi provizas normigitan manieron por uzantoj publikigi enhavon, sekvi aliajn uzantojn,
kaj okupiĝi pri sociaj interagoj kiel ŝatado, kundivido kaj komentado pri fadenoj
aŭ afiŝoj."
password_confirm_header: Konfirmu vian peton de ŝanĝo de pasvorto.
oauth2.grant.moderate.magazine.reports.all: Administri raportojn en la revuoj,
kiujn vi moderigas.
resend_account_activation_email_error: Estis problemo dum ĉi tiu peto. Eble ne
ekzistas konto asociita kun tiu retpoŝtadreso aŭ eble ĝi jam estas aktivigita.
federation_page_enabled: Federada paĝo ebligita
email_confirm_button_text: Konfirmu vian peton de ŝanĝo de pasvorto
toolbar.bold: Grasa
errors.server429.title: 429 Tro da Petoj
toolbar.header: Kapo
oauth.consent.to_allow_access: Por permesi ĉi tiun aliron, alklaku la butonon
'Permesi' sube
email.delete.description: La sekva uzanto petis, ke ilia konto estu forigita
toolbar.ordered_list: Ordigita Listo
oauth.consent.app_requesting_permissions: ŝatus plenumi la sekvajn agojn en via
nomo
federated_search_only_loggedin: Federacia serĉo limigita se ne ensalutinta
oauth2.grant.moderate.magazine.reports.action: Akcepti aŭ malakcepti raportojn
en la revuoj, kiujn vi moderigas.
your_account_is_not_active: Via konto ne estas aktivigita. Bonvolu kontroli vian
retpoŝton por instrukcioj pri aktivigo de konto aŭ peti novan retmesaĝon pri aktivigo de konto.
oauth.consent.allow: Permesi
custom_css: Adaptita CSS
block: Bloki
toolbar.quote: Citaĵo
oauth2.grant.moderate.magazine.list: Legi liston de la revuoj, kiujn vi
moderigas.
toolbar.unordered_list: Neordigita Listo
errors.server404.title: 404 Ne Trovita
resend_account_activation_email_success: Se konto asociita kun tiu retpoŝtadreso
ekzistas, ni sendos novan aktivigan retmesaĝon.
errors.server403.title: 403 Malpermesita
oauth2.grant.moderate.magazine.reports.read: Legi raportojn en la revuoj, kiujn
vi moderigas.
ignore_magazines_custom_css: Ignori la adaptitan CSS-on de revuoj
oauth.consent.deny: Nei
oauth.consent.title: OAuth2 konsentformularo
federation_page_allowed_description: Konataj nodoj kun kiuj ni federacias
resend_account_activation_email: Resendi retmesaĝon pri aktivigo de konto
errors.server500.title: 500 Interna Servila Eraro
toolbar.link: Ligilo
toolbar.mention: Mencio
resend_account_activation_email_question: Neaktiva konto?
resend_account_activation_email_description: Enigu la retpoŝtadreson asociitan
kun via konto. Ni sendos alian aktivigan retmesaĝon por vi.
your_account_has_been_banned: Via konto forbaris
toolbar.code: Kodo
errors.server500.description: Pardonu, io misfunkciis ĉe nia flanko. Se vi daŭre
vidas ĉi tiun eraron, provu kontakti la posedanton de la nodo. Se ĉi tiu nodo
tute ne funkcias, kontrolu %link_start%aliajn Mbin-nodojn%link_end% dume ĝis
la problemo estos solvita.
oauth.client_not_granted_message_read_permission: Ĉi tiu programo ne ricevis
permeson legi viajn mesaĝojn.
restrict_oauth_clients: Limigi kreadon de Kliento OAuth2 al Administrantoj
federation_page_disallowed_description: Nodoj kun kiuj ni ne federacias
unblock: Malbloki
oauth.consent.grant_permissions: Doni Permesojn
oauth2.grant.moderate.magazine.ban.delete: Malforbari uzantojn en la revuoj,
kiujn vi moderigas.
oauth.client_identifier.invalid: Nevalida OAuth Kliento-ID!
oauth.consent.app_has_permissions: jam povas plenumi la jenajn agojn
email.delete.title: Peto pri forigo de uzantkonto
email_confirm_link_help: Alternative vi povas kopii kaj alglui la jenajn en vian
retumilon
toolbar.strikethrough: Trastreki
toolbar.image: Bildo
toolbar.italic: Kursiva
more_from_domain: Pli de domajno
oauth2.grant.moderate.magazine.trash.read: Rigardi rubujigatan enhavon en
revuoj, kiujn vi moderigas.
oauth2.grant.moderate.magazine_admin.create: Krei novajn revuojn.
oauth2.grant.moderate.magazine_admin.edit_theme: Redakti la adaptitan CSS-on de
iu ajn el viaj posedataj revuoj.
oauth2.grant.moderate.magazine_admin.tags: Krei aŭ forigi etikedojn de viaj
posedataj revuoj.
oauth2.grant.admin.entry.purge: Tute forigu ajnan fadenon de via nodo.
oauth2.grant.moderate.magazine_admin.delete: Forigi iujn ajn revuojn, kiujn vi
posedas.
oauth2.grant.moderate.magazine_admin.all: Krei, redakti aŭ forigi revuojn, kiujn
vi posedas.
oauth2.grant.report.general: Raporti fadenojn, afiŝojn aŭ komentojn.
oauth2.grant.admin.all: Fari ajnan administran agon sur via nodo.
oauth2.grant.moderate.magazine_admin.update: Redakti iun ajn el viaj posedataj
revuoj reguloj, priskribo, NSFL-statuso aŭ ikono.
oauth2.grant.write.general: Krei aŭ redakti iun ajn el viaj fadenoj, afiŝoj aŭ
komentoj.
oauth2.grant.read.general: Legi ĉiujn enhavojn, al kiuj vi havas aliron.
oauth2.grant.delete.general: Forigi iujn viajn fadenojn, afiŝojn aŭ komentojn.
oauth2.grant.moderate.magazine_admin.stats: Rigardi la statistikojn de enhavo,
voĉoj kaj vidoj de viaj posedataj revuoj.
oauth2.grant.moderate.magazine_admin.moderators: Aldoni aŭ forigi moderigantojn
de iu ajn el viaj posedataj revuoj.
oauth2.grant.moderate.magazine_admin.badges: Krei aŭ forigi insignojn de viaj
posedataj revuoj.
flash_image_download_too_large_error: La bildo ne povis esti kreita, ĝi estas
tro granda (maksimuma grandeco %bytes%)
show_subscriptions: Montri abonojn
flash_thread_new_error: La fadeno ne povis esti kreita. Io misfunkciis.
oauth2.grant.moderate.post_comment.change_language: Ŝanĝi la lingvon de komentoj
pri afiŝoj en la revuoj, kiujn vi moderigas.
downvotes_mode: Reĝimo kontraŭvoĉdonoj
disabled: Malebligita
hidden: Kaŝita
enabled: Ebligita
change_downvotes_mode: Ŝanĝi reĝimon de kontraŭvoĉdonoj
tag: Etikedo
edit_entry: Redakti fadenon
default_theme: Defaŭlta etoso
default_theme_auto: Hela/Malhela (Aŭtomatrekoni)
flash_mark_as_adult_success: La afiŝo estis sukcese markita kiel NSPL.
flash_unmark_as_adult_success: La afiŝo estis sukcese malmarkita kiel NSPL.
unban: Malforbari
ban_hashtag_btn: Forbari haŝetikedon
account_deletion_title: Forigo de konto
oauth2.grant.subscribe.general: Aboni aŭ sekvi ajnan revuon, domajnon aŭ
uzanton, kaj rigardi la revuojn, domajnojn kaj uzantojn, al kiuj vi abonas.
oauth2.grant.moderate.post.all: Kontroli afiŝojn en la revuoj, kiujn vi
moderigas.
oauth2.grant.moderate.post_comment.all: Kontroli komentojn pri afiŝoj en la
revuoj, kiujn vi moderigas.
single_settings: Unuopa
oauth2.grant.moderate.magazine.all: Administri forbarojn, raportojn, kaj rigardi
rubujigataj eroj en la revuoj, kiujn vi moderigas.
oauth2.grant.moderate.magazine.ban.all: Administri forbaritajn uzantojn en la
revuoj, kiujn vi moderigas.
account_deletion_immediate: Forigi tuj
from: de
subscription_sidebar_pop_out_right: Movi al aparta flanka kolumno dekstre
menu: Menuo
unban_hashtag_btn: Malforbari Haŝetikedon
ban_hashtag_description: Forbaro de haŝetikedo malhelpos kreadon de afiŝoj kun
ĉi tiu haŝetikedo, kaj ankaŭ kaŝos ekzistantajn afiŝojn kun ĉi tiu haŝetikedo.
unmark_as_adult: Malmarki NSPL
account_deletion_button: Forigi Konton
oauth2.grant.domain.all: Aboni aŭ bloki domajnojn, kaj rigardi la domajnojn,
kiujn vi abonas aŭ blokas.
oauth2.grant.entry_comment.all: Krei, redakti aŭ forigi viajn komentojn en
fadenoj, kaj voĉdoni, akceli aŭ raporti ajnan komenton en fadeno.
oauth2.grant.entry.vote: Porvoĉdoni, akceli aŭ kontraŭvoĉdoni ajnan fadenon.
oauth2.grant.user.message.create: Sendi mesaĝojn al aliaj uzantoj.
oauth2.grant.user.notification.all: Legi kaj malplenigi viajn sciigojn.
oauth2.grant.user.message.read: Legi viajn mesaĝojn.
oauth2.grant.moderate.entry.all: Kontroli fadenojn en la revuoj, kiujn vi
moderigas.
oauth2.grant.user.oauth_clients.edit: Redakti la permesojn, kiujn vi donis al
aliaj OAuth2-aplikaĵoj.
oauth2.grant.user.block: Bloki aŭ malbloki uzantojn, kaj legi liston de uzantoj,
kiujn vi blokas.
oauth2.grant.moderate.entry.change_language: Ŝanĝi la lingvon de fadenoj en la
revuoj, kiujn vi moderigas.
oauth2.grant.moderate.entry.trash: Rubuji aŭ restarigi fadenojn en la revuoj,
kiujn vi moderigas.
oauth2.grant.moderate.entry_comment.trash: Rubuji aŭ restarigi komentojn en
fadenoj de la revuoj, kiujn vi moderigas.
oauth2.grant.moderate.entry_comment.change_language: Ŝanĝi la lingvon de
komentoj en fadenoj de la revuoj, kiujn vi moderigas.
oauth2.grant.admin.magazine.move_entry: Movi fadenojn inter revuojn sur via
nodo.
oauth2.grant.admin.user.verify: Konfirmi uzantojn sur via nodo.
oauth2.grant.admin.user.delete: Forigi uzantojn de via nodo.
oauth2.grant.admin.federation.read: Rigardi la liston de defederitaj nodoj.
oauth2.grant.admin.federation.update: Aldoni aŭ forigi nodojn al aŭ de la listo
de defederitaj nodoj.
oauth2.grant.admin.instance.settings.all: Rigardi aŭ ĝisdatigi agordojn sur via
nodo.
oauth2.grant.admin.oauth_clients.revoke: Revoki aliron al OAuth2-klientoj en via
nodo.
show_avatars_on_comments: Montri Komentajn Avatarojn
moderation.report.reject_report_title: Malakcepti La Raporton
moderation.report.approve_report_confirmation: Ĉu vi certas, ke vi volas aprobi
ĉi tiun raporton?
purge_content_desc: Tute viŝi la enhavon de la uzanto, inkluzive de forigi la
respondoj de aliaj uzantoj en kreitaj fadenoj, afiŝoj kaj komentoj.
schedule_delete_account: Plani Forigon
remove_schedule_delete_account: Forigi Planitan Forigon
two_factor_authentication: Dupaŝa aŭtentigo
2fa.disable: Malebligi dupaŝan aŭtentigon
2fa.backup: Viaj dupaŝaj rezervaj kodoj
2fa.enable: Agordi dupaŝan aŭtentigon
2fa.verify_authentication_code.label: Enigi dupaŝan kodon por konfirmi agordon
2fa.qr_code_link.title: Vizitante ĉi tiun ligon povas permesi al via platformo
registri ĉi tiun dupaŝan aŭtentigon
2fa.backup_codes.help: Vi povas uzi ĉi tiujn kodojn kiam vi ne havas vian
dupaŝan aŭtentikan aparaton aŭ aplikaĵon. Oni ne plu montros
ilin al vi kaj vi povos uzi ĉiun el ili nur unufoje .
subscriptions_in_own_sidebar: En aparta flanka kolumno
sidebars_same_side: Flankaj kolumnoj al la sama flanko
subscription_sidebar_pop_in: Movi abonojn al la enlinia panelo
pending: Pritraktata
position_top: Supro
solarized_auto: Sunigita (Aŭtomatrekoni)
unban_hashtag_description: Malforbaro de haŝetikedo permesos krei afiŝojn kun ĉi
tiu haŝetikedo denove. Ekzistantaj afiŝoj kun ĉi tiu hashetikedo ne plu estas
kaŝitaj.
oauth2.grant.admin.instance.all: Rigardi kaj ĝisdatigi agordojn aŭ informojn de
nodo.
oauth2.grant.admin.instance.settings.read: Rigradi agordojn sur via nodo.
oauth2.grant.moderate.post.trash: Rubuji aŭ restarigi afiŝojn en la revuoj,
kiujn vi moderigas.
oauth2.grant.admin.magazine.purge: Tute forigi revuojn sur via nodo.
moderation.report.reject_report_confirmation: Ĉu vi certas, ke vi volas
malakcepti ĉi tiun raporton?
password_and_2fa: Pasvorto & 2PA
subscription_sort: Ordigi
subscription_panel_large: Granda panelo
oauth2.grant.entry.create: Krei novajn fadenojn.
oauth2.grant.moderate.magazine.ban.create: Forbari uzantojn en la revuoj, kiujn
vi moderigas.
oauth2.grant.block.general: Bloki aŭ malbloki ajnan revuon, domajnon aŭ uzanton,
kaj rigardi la revuojn, domajnojn kaj uzantojn, kiujn vi blokis.
toolbar.spoiler: Malkaŝo de intrigo
account_deletion_description: Via konto estos forigita post 30 tagoj krom se vi
elektas forigi la konton tuj. Por restarigi vian konton ene de 30 tagoj,
ensalutu kun la samaj uzantkreditaĵoj aŭ kontaktu administranton.
oauth2.grant.post_comment.delete: Forigi viajn ekzistantajn komentojn pri
afiŝoj.
oauth2.grant.vote.general: Porvoĉdoni, kontraŭvoĉdoni, aŭ akceli fadenon,
afiŝojn aŭ komentojn.
oauth2.grant.post.edit: Redakti viajn ekzistantajn afiŝojn.
oauth2.grant.post_comment.all: Krei, redakti aŭ forigi viajn komentojn pri
afiŝoj, kaj voĉdoni, akceli aŭ raporti ajnan komenton pri afiŝo.
oauth2.grant.user.oauth_clients.all: Legi kaj redakti la permesojn, kiujn vi
donis al aliaj OAuth2-aplikaĵoj.
oauth2.grant.user.oauth_clients.read: Legi la permesojn, kiujn vi donis al aliaj
OAuth2-aplikaĵoj.
oauth2.grant.moderate.entry_comment.all: Kontroli komentojn en fadenoj en la
revuoj, kiujn vi moderigas.
oauth2.grant.moderate.entry.set_adult: Marki fadenojn kiel NSPL en la revuoj,
kiujn vi moderigas.
oauth2.grant.post.delete: Forigi viajn ekzistantajn afiŝojn.
oauth2.grant.moderate.entry.pin: Alpingli fadenojn al la supro de la revuoj,
kiujn vi moderigas.
oauth2.grant.post.report: Raporti ajnan afiŝon.
oauth2.grant.moderate.post_comment.set_adult: Marki komentojn pri afiŝoj kiel
NSPL en la revuoj, kiujn vi moderigas.
oauth2.grant.moderate.post_comment.trash: Rubuji aŭ restarigi komentojn pri
afiŝoj en la revuoj, kiujn vi moderigas.
moderation.report.approve_report_title: Aprobi La Raporton
oauth2.grant.admin.oauth_clients.all: Rigardi aŭ revoki OAuth2-klientojn, kiuj
ekzistas sur via nodo.
oauth2.grant.admin.oauth_clients.read: Rigardi OAuth2-klientojn, kiuj ekzistas
sur via nodo, kaj iliajn uzstatistikojn.
comment_reply_position_help: Montri la komentan respondformularon aŭ supre aŭ
malsupre de la paĝo. Kiam 'senfina movo' estas ebligita la pozicio ĉiam aperos
supre.
update_comment: Ĝisdatigi komenton
remove_schedule_delete_account_desc: Forigi la planitan forigon. La tuta enhavo
estos disponebla denove kaj la uzanto povos ensaluti.
oauth2.grant.domain.block: Bloki aŭ malbloki domajnojn kaj rigardi la domajnojn,
kiujn vi blokis.
oauth2.grant.entry.all: Krei, redakti aŭ forigi viajn fadenojn, kaj voĉdoni,
akceli aŭ raporti ajnan fadenon.
oauth2.grant.entry_comment.delete: Forigi viajn ekzistantajn komentojn en
fadenoj.
oauth2.grant.entry_comment.vote: Porvoĉdoni, akceli aŭ kontraŭvoĉdoni ajnan
komenton en fadeno.
oauth2.grant.entry_comment.report: Raporti ajnan komenton en fadeno.
oauth2.grant.post.create: Krei novajn afiŝojn.
oauth2.grant.magazine.block: Bloki aŭ malbloki revuojn kaj rigardi la revuojn,
kiujn vi blokis.
oauth2.grant.post.vote: Porvoĉdoni, akceli aŭ kontraŭvoĉdoni iun ajn afiŝon.
oauth2.grant.post_comment.vote: Porvoĉdoni, akceli aŭ kontraŭvoĉdoni ajnan
komenton pri afiŝo.
oauth2.grant.post_comment.report: Raporti ajnan komenton pri afiŝo.
oauth2.grant.user.notification.read: Legi viajn sciigojn, inkluzive de mesaĝaj
sciigoj.
oauth2.grant.moderate.all: Fari ajnan moderigan agon, kiun vi rajtas fari en la
revuoj, kiujn vi moderigas.
oauth2.grant.admin.post_comment.purge: Tute forigi ajnan komenton pri afiŝo de
via nodo.
oauth2.grant.admin.user.ban: Forbari aŭ malforbari uzantojn de via nodo.
oauth2.grant.admin.instance.settings.edit: Ĝisdatigi agordojn sur via nodo.
oauth2.grant.admin.instance.information.edit: Ĝisdatigi la paĝojn Pri, Oftaj
Demandoj, Kontakto, Servadokondiĉoj kaj Privateca Politiko sur via nodo.
last_active: Laste Aktiva
flash_post_unpin_success: La afiŝo estis sukcese malpinglita.
subject_reported_exists: Ĉi tiu enhavo jam estis raportita.
moderation.report.ban_user_title: Forbari Uzanton
purge_content: Viŝi enhavon
delete_content_desc: Forigi la enhavon de la uzanto lasante la respondojn de
aliaj uzantoj en la kreitaj fadenoj, afiŝoj kaj komentoj.
2fa.authentication_code.label: Aŭtentikiga Kodo
2fa.verify: Konfirmi
2fa.qr_code_img.alt: QR-kodo, kiu permesas la agordon de dupaŝa aŭtentigo por
via konto
2fa.backup_codes.recommendation: Oni rekomendas, ke vi konservu kopion de ili en
sekura loko.
cancel: Nuligi
schedule_delete_account_desc: Plani la forigon de ĉi tiu konto post 30 tagoj. Ĉi
tio kaŝos la uzanton kaj ilian enhavon kaj ankaŭ preventos la uzanton
ensaluti.
2fa.code_invalid: La aŭtentikigkodo ne validas
moderation.report.ban_user_description: Ĉu vi volas forbari la uzanton
(%username%) kiu kreis ĉi tiun enhavon de ĉi tiu revuo?
flash_thread_tag_banned_error: La fadeno ne povis esti kreita. La enhavo ne
estas permesita.
delete_account_desc: Forigi la konton, inkluzive de la respondoj de aliaj
uzantoj en kreitaj fadenoj, afiŝoj kaj komentoj.
alphabetically: Alfabete
close: Fermi
mark_as_adult: Marki NSPL
oauth2.grant.domain.subscribe: Aboni aŭ malaboni domajnojn kaj rigardi la
domajnojn al kiuj vi abonas.
oauth2.grant.entry.edit: Redakti viajn ekzistantajn fadenojn.
oauth2.grant.magazine.all: Aboni aŭ bloki revuojn, kaj rigardi la revuojn, kiujn
vi abonas aŭ blokas.
oauth2.grant.magazine.subscribe: Aboni aŭ malaboni revuojn kaj rigardi la
revuojn, kiujn vi abonas.
oauth2.grant.user.profile.all: Legi kaj redakti vian profilon.
oauth2.grant.admin.magazine.all: Movi fadenojn inter aŭ tute forigi revuojn sur
via nodo.
flash_post_pin_success: La afiŝo estis sukcese alpinglita.
comment_reply_position: Komenta respondpozicio
flash_account_settings_changed: Viaj kontaj agordoj estis sukcese ŝanĝitaj. Vi
devos denove ensaluti.
subscription_sidebar_pop_out_left: Movi al aparta flanka kolumno maldekstre
flash_email_was_sent: Retmesaĝo estas sukcese sendita.
oauth2.grant.post.all: Krei, redakti aŭ forigi viajn mikroblogojn, kaj voĉdoni,
akceli aŭ raporti ajnan mikroblogon.
2fa.remove: Forigi 2PA
oauth2.grant.user.follow: Sekvi aŭ malsekvi uzantojn, kaj legi liston de
uzantoj, kiujn vi sekvas.
subscribers_count: '{0}Abonantoj|{1}Abonanto|]1,Inf[ Abonantoj'
followers_count: '{0}Sekvantoj|{1}Sekvanto|]1,Inf[ Sekvantoj'
marked_for_deletion: Markita por forigo
marked_for_deletion_at: Markita por forigo je %date%
sort_by: Ordigi laŭ
filter_by_subscription: Filtri laŭ abono
filter_by_federation: Filtri laŭ federada stato
two_factor_backup: Dupaŝaj aŭtentigaj rezervaj kodoj
position_bottom: Malsupro
subscribe_for_updates: Abonu por komenci ricevi ĝisdatigojn.
remove_media: Forigi aŭdvidaĵon
always_disconnected_magazine_info: Ĉi tiu revuo ne ricevas ĝisdatigojn.
disconnected_magazine_info: Ĉi tiu revuo ne ricevas ĝisdatigojn (lasta aktiveco
antaŭ %days% tagoj).
oauth2.grant.post_comment.create: Krei novajn komentojn pri afiŝoj.
oauth2.grant.entry_comment.create: Krei novajn komentojn en fadenoj.
oauth2.grant.entry_comment.edit: Redakti viajn ekzistantajn komentojn en
fadenoj.
oauth2.grant.admin.instance.stats: Rigardi la statistikojn de via nodo.
2fa.available_apps: Uzu dupaŝan aŭtentikigaplikaĵon kiel %google_authenticator%,
%aegis% (Android) aŭ %raivo% (iOS) por skani la QR-kodon.
2fa.user_active_tfa.title: Uzanto havas aktivan 2PA-on
federation_page_dead_title: Mortintaj nodoj
federation_page_dead_description: Nodoj, kie ni ne povis liveri almenaŭ 10
agadojn sinsekve kaj kie la lastaj sukcesaj liverado kaj ricevado estis antaŭ
pli ol unu semajno
private_instance: Devigi uzantojn ensaluti antaŭ ol ili povas aliri ajnan
enhavon
oauth2.grant.entry.delete: Forigi viajn ekzistantajn fadenojn.
oauth2.grant.entry.report: Raporti ajnan fadenon.
oauth2.grant.post_comment.edit: Redakti viajn ekzistantajn komentojn pri afiŝoj.
oauth2.grant.user.all: Legi kaj redakti vian profilon, mesaĝojn aŭ sciigojn;
Legi kaj redakti permesojn, kiujn vi donis al aliaj aplikaĵoj; sekvi aŭ bloki
aliajn uzantojn; rigardi listojn de uzantoj, kiujn vi sekvas aŭ blokas.
oauth2.grant.user.profile.edit: Redakti vian profilon.
oauth2.grant.user.message.all: Legi viajn mesaĝojn kaj sendi mesaĝojn al aliaj
uzantoj.
oauth2.grant.user.notification.delete: Malplenigi viajn sciigojn.
oauth2.grant.user.profile.read: Legi vian profilon.
oauth2.grant.moderate.entry_comment.set_adult: Marki komentojn en fadenoj kiel
NSPL en la revuoj, kiujn vi moderigas.
oauth2.grant.moderate.post.change_language: Ŝanĝi la lingvon de afiŝoj en la
revuoj, kiujn vi moderigas.
oauth2.grant.moderate.post.set_adult: Marki afiŝojn kiel NSPL en la revuoj,
kiujn vi moderigas.
oauth2.grant.moderate.magazine.ban.read: Rigardi forbaritajn uzantojn en la
revuoj, kiujn vi moderigas.
oauth2.grant.admin.entry_comment.purge: Tute forigi ajnan komenton en fadeno de
via nodo.
oauth2.grant.admin.post.purge: Tute forigu ajnan afiŝon de via nodo.
oauth2.grant.admin.user.all: Forbari, konfirmi aŭ tute forigi uzantojn sur via
nodo.
oauth2.grant.admin.user.purge: Tute forigi uzantojn de via nodo.
oauth2.grant.admin.federation.all: Rigardi kaj ĝisdatigi nuntempe defederitajn
nodojn.
show_avatars_on_comments_help: Montri/kaŝi uzantajn avatarojn kiam rigardado
komentojn pri ununura fadeno aŭ afiŝo.
magazine_theme_appearance_custom_css: Adaptita CSS-o kiu aplikiĝos dum rigardado
de enhavo ene de via revuo.
magazine_theme_appearance_icon: Adaptita ikono por la revuo.
magazine_theme_appearance_background_image: Adaptita fonbildo kiu estos aplikata
dum rigardado de enhavo ene de via revuo.
oauth2.grant.moderate.post.pin: Alpingli afiŝojn al la supro de la revuoj, kiujn
vi moderigas.
delete_content: Forigi enhavon
2fa.setup_error: Eraro ebligante 2PA por konto
2fa.backup-create.label: Krei novajn rezervajn aŭtentigajn kodojn
2fa.add: Aldoni al mia konto
subscription_header: Abonitaj Revuoj
2fa.backup-create.help: Vi povas krei novajn rezervajn aŭtentigajn kodojn; fari
tion nevalidigos ekzistantajn kodojn.
remove_user_avatar: Forigi avataron
remove_user_cover: Forigi kovrilon
notify_on_user_signup: Novaj aliĝoj
type_search_term_url_handle: Tajpi serĉvorton, URL-on aŭ uzantnomon
viewing_one_signup_request: Vi nur rigardas unu aliĝpeton fare de %username%
your_account_is_not_yet_approved: Via konto ankoraŭ ne estas aprobita. Ni sendos
al vi retmesaĝon tuj kiam la administrantoj prilaboros vian aliĝpeton.
toolbar.emoji: Emoĝio
oauth2.grant.user.bookmark: Aldoni kaj forigi legosignojn
oauth2.grant.user.bookmark.add: Aldoni legosignojn
oauth2.grant.user.bookmark.remove: Forigi legosignojn
oauth2.grant.user.bookmark_list: Legi, redakti kaj forigi viajn
legosigno-listojn
oauth2.grant.user.bookmark_list.read: Legi viajn legosigno-listojn
oauth2.grant.user.bookmark_list.edit: Redakti viajn legosigno-listojn
oauth2.grant.user.bookmark_list.delete: Forigi viajn legosigno-listojn
2fa.manual_code_hint: Se vi ne povas skani la QR-kodon, enigu la sekreton
permane
flash_email_failed_to_sent: Retmesaĝo ne eblis sendi.
flash_post_new_success: Afiŝo sukcese kreita.
flash_post_new_error: Afiŝo ne povis esti kreita. Io misfunkciis.
flash_magazine_theme_changed_success: Sukcese ĝisdatigis la aspekton de la
revuo.
flash_magazine_theme_changed_error: Malsukcesis ĝisdatigi la aspekton de la
revuo.
flash_comment_new_success: Komento estis sukcese kreita.
flash_comment_edit_success: Komento estis sukcese ĝisdatigita.
flash_comment_new_error: Malsukcesis krei komenton. Io misfunkciis.
flash_comment_edit_error: Malsukcesis redakti komenton. Io misfunkciis.
flash_user_settings_general_success: Uzantagordoj sukcese konservitis.
flash_user_settings_general_error: Malsukcesis konservi uzantagordojn.
flash_user_edit_profile_error: Malsukcesis konservi profilagordojn.
flash_user_edit_profile_success: Uzantprofilagordoj sukcese konservitis.
flash_user_edit_email_error: Malsukcesis ŝanĝi retpoŝtadreson.
flash_user_edit_password_error: Malsukcesis ŝanĝi pasvorton.
flash_thread_edit_error: Malsukcesis redakti la fadenon. Io misfunkciis.
flash_post_edit_error: Malsukcesis redakti la afiŝon. Io misfunkciis.
flash_post_edit_success: La afiŝo estis sukcese redaktita.
page_width: Paĝolarĝo
page_width_max: Max
page_width_auto: Aŭtomata
page_width_fixed: Fiksita
filter_labels: Filtri Etikedojn
auto: Aŭtomata
open_url_to_fediverse: Malfermi originalan URL-on
change_my_avatar: Ŝanĝi mian avataron
change_my_cover: Ŝanĝi mian kovrilon
edit_my_profile: Redakti mian profilon
account_settings_changed: Viaj konto-agordoj estis sukcese ŝanĝitaj. Vi devos
ensaluti denove.
magazine_deletion: Forigo de revuo
delete_magazine: Forigi revuon
restore_magazine: Restaŭri revuon
purge_magazine: Viŝi revuon
magazine_is_deleted: La revuo estas forigita. Vi povas restaŭri ĝin ene de 30 tagoj.
suspend_account: Halteti konton
unsuspend_account: Malhalteti konton
account_suspended: La konto estas haltetita.
account_unsuspended: La konto estas malhaltetita.
deletion: Forigo
user_suspend_desc: Halteti vian konton kaŝas vian enhavon en la nodo, sed ne
forigas ĝin porĉiame, kaj vi povas restarigi ĝin iam ajn.
account_banned: La konto estas forbarita.
account_unbanned: La konto estas malforbarita.
account_is_suspended: Uzantokonto haltetas.
remove_following: Forigi sekvadon
remove_subscriptions: Forigi abonojn
apply_for_moderator: Kandidatiĝi por moderiganto
request_magazine_ownership: Peti proprieton de revuo
cancel_request: Nuligi peton
abandoned: Forlasita
ownership_requests: Petoj pri proprieto
accept: Akcepti
moderator_requests: Petoj de moderiganto
action: Ago
user_badge_op: OA
user_badge_admin: Admin
user_badge_global_moderator: Ĝenerala Moderiganto
user_badge_moderator: Moderiganto
user_badge_bot: Roboto
announcement: Anonco
keywords: Ŝlosilvortoj
deleted_by_moderator: Fadeno, afiŝo aŭ komento forigitis de la moderiganto
deleted_by_author: Fadeno, afiŝo aŭ komento forigitis de la aŭtoro
sensitive_warning: Sentema enhavo
sensitive_toggle: Baskuligi videblecon de sentema enhavo
sensitive_show: Alklaki por montri
sensitive_hide: Alklaki por kaŝi
details: Detaloj
spoiler: Malkaŝo de intrigo
all_time: Ĉiuj tempoj
show: Montri
hide: Kaŝi
edited: redaktita
sso_registrations_enabled: Unuensalutaj registradoj ebligitaj
sso_registrations_enabled.error: Novaj konto-registradoj ĉe triapartaj
identec-administriloj estas nuntempe malebligitaj.
sso_only_mode: Limigi ensaluton kaj registriĝon nur al unuensaluta-metodoj
related_entry: Rilata
restrict_magazine_creation: Limigi lokan revuokreadon al administrantoj kaj
ĝeneralaj moderigantoj
sso_show_first: Montri unuensaluto unue sur ensalutaj kaj registriĝaj paĝoj
continue_with: Daŭrigu kun
reported_user: Raportita uzanto
reporting_user: Raportanta uzanto
reported: raportita
report_subject: Temo
own_report_rejected: Via raporto malakceptitis
own_report_accepted: Via raporto akceptitis
own_content_reported_accepted: Raporto pri via enhavo akceptitis.
report_accepted: Raporto akceptitis
open_report: Malfermi raporton
cake_day: Kukotago
someone: Iu
back: Reen
magazine_log_mod_added: aldonis moderiganton
magazine_log_mod_removed: forigis moderiganton
magazine_log_entry_pinned: alpinglita eniro
magazine_log_entry_unpinned: forigita alpinglita eniro
last_updated: Laste ĝisdatigita
and: kaj
direct_message: Rektmesaĝo
manually_approves_followers: Mane aprobas sekvantojn
register_push_notifications_button: Registriĝi por Puŝaj Sciigoj
unregister_push_notifications_button: Forigi Puŝan Registriĝon
test_push_notifications_button: Testi Puŝajn Sciigojn
test_push_message: Saluton Mondo!
notification_title_new_comment: Nova komento
notification_title_removed_comment: Komento forigitis
notification_title_edited_comment: Komento redaktitis
notification_title_mention: Vi menciitis
notification_title_new_reply: Nova Respondo
notification_title_new_thread: Nova fadeno
notification_title_removed_thread: Fadeno forigitis
notification_title_edited_thread: Fadeno redaktitis
notification_title_ban: Vi estis forbarita
notification_title_message: Nova rektmesaĝo
notification_title_new_post: Nova Afiŝo
notification_title_removed_post: Afiŝo estis forigita
notification_title_edited_post: Afiŝo estis redaktita
notification_title_new_signup: Nova uzanto registriĝis
notification_body_new_signup: La uzanto %u% registriĝis.
notification_body2_new_signup_approval: Vi devas aprobi la peton antaŭ ol ili
povas ensaluti
show_related_magazines: Montri hazardajn revuojn
show_related_entries: Montri hazardajn fadenojn
show_related_posts: Montri hazardajn afiŝojn
show_active_users: Montri aktivajn uzantojn
notification_title_new_report: Nova raporto estis kreita
magazine_posting_restricted_to_mods_warning: Nur moderigantoj rajtas krei
fadenojn en ĉi tiu revuo
flash_posting_restricted_error: Krei fadenojn estas limigita al moderigantoj en
ĉi tiu revuo kaj vi ne estas unu el ili
server_software: Servila programaro
version: Versio
last_successful_deliver: Lasta sukcesa liverado
last_successful_receive: Lasta sukcesa ricevo
last_failed_contact: Lasta malsukcesa kontakto
magazine_posting_restricted_to_mods: Limigi fadenkreadon al moderigantoj
new_user_description: Ĉi tiu uzanto estas nova (aktiva dum malpli ol %days%
tagoj)
new_magazine_description: Ĉi tiu revuo estas nova (aktiva dum malpli ol %days%
tagoj)
admin_users_active: Aktiva
admin_users_inactive: Neaktiva
admin_users_suspended: Haltetigita
admin_users_banned: Forbarita
user_verify: Aktivigi konton
max_image_size: Maksimuma dosiergrandeco
comment_not_found: Komento ne trovita
bookmark_add_to_list: Aldoni legosignon al %list%
bookmark_remove_from_list: Forigi legosignon el %list%
bookmark_remove_all: Forigi ĉiujn legosignojn
bookmark_add_to_default_list: Aldoni legosignon al defaŭlta listo
bookmark_lists: Legosigno-Listoj
bookmarks: Legosignoj
bookmarks_list: Legosignoj en %list%
count: Nombro
is_default: Estas Defaŭlta
bookmark_list_is_default: Estas defaŭlta listo
bookmark_list_make_default: Fari Defaŭlta
bookmark_list_create: Krei
bookmark_list_create_placeholder: tajpi nomon...
bookmark_list_create_label: Listonomo
bookmarks_list_edit: Redakti legosignoliston
bookmark_list_edit: Redakti
bookmark_list_selected_list: Elektita listo
table_of_contents: Enhavotabelo
search_type_all: Ĉio
search_type_entry: Fadenoj
search_type_post: Mikroblogoj
search_type_magazine: Revuoj
search_type_user: Uzantoj
search_type_actors: Revuoj + Uzantoj
search_type_content: Fadenoj + Mikroblogoj
select_user: Elekti uzanton
new_users_need_approval: Novaj uzantoj devas esti aprobitaj de administranto
antaŭ ol ili povas ensaluti.
signup_requests: Aliĝpetoj
application_text: Klarigu kial vi volas aliĝi
signup_requests_header: Aliĝpetoj
signup_requests_paragraph: Ĉi tiuj uzantoj ŝatus aliĝi al via servilo. Ili ne
povas ensaluti ĝis vi aprobis ilian aliĝpeton.
flash_application_info: Administranto bezonas aprobi vian konton antaŭ ol vi
povas ensaluti. Vi ricevos retmesaĝon post kiam via aliĝpeto estos
prilaborita.
email_application_approved_title: Via aliĝpeto estas aprobita
email_application_approved_body: Via aliĝpeto estis aprobita de la servila
administranto. Vi nun povas ensaluti en la servilon ĉe %siteName% .
email_application_rejected_title: Via aliĝpeto estis malakceptita
email_application_rejected_body: Dankon pro via intereso, sed ni bedaŭras
informi vin, ke via aliĝpeto estis rifuzita.
email_application_pending: Via konto bezonas administran aprobon antaŭ ol vi
povas ensaluti.
email_verification_pending: Vi devas konfirmi vian retpoŝtadreson antaŭ ol vi
povas ensaluti.
show_magazine_domains: Montri revuajn domajnojn
show_user_domains: Montri uzantajn domajnojn
answered: respondita
by: de
front_default_sort: Defaŭlta ordigo de la ĉefpaĝo
comment_default_sort: Defaŭlta ordigo de komentoj
open_signup_request: Malfermi aliĝpeton
image_lightbox_in_list: Fadenaj bildetoj malfermiĝas plenekrane
compact_view_help: Kompakta vido kun malpli da marĝenoj, kie la aŭdvidaĵo estas
movita dekstren.
show_users_avatars_help: Montri la bildon de la uzanto-avataro.
show_magazines_icons_help: Montri la revuo-bildsimbolo.
show_thumbnails_help: Montri la bildetojn.
image_lightbox_in_list_help: Kiam markita, alklako de la bildeto montras modalan
bildkestofenestron. Kiam nemarkita, alklako de la bildeto malfermos la
fadenon.
show_new_icons: Montri novajn bildsimbolojn
show_new_icons_help: Montri bildsimbolon por nova revuo/uzanto (30 tagojn aĝa aŭ
pli nova)
magazine_instance_defederated_info: La instanco de ĉi tiu revuo estas
defederita. La revuo tial ne ricevos ĝisdatigojn.
user_instance_defederated_info: La instanco de ĉi tiu uzanto estas defederita.
flash_thread_instance_banned: La instanco de ĉi tiu revuo estas forbarita.
show_rich_mention: Riĉaj mencioj
show_rich_mention_help: Bildigi uzantan komponenton kiam uzanto estas menciita.
Tio inkluzivos ties montritan nomon kaj profilbildon.
show_rich_mention_magazine: Riĉaj revuaj mencioj
show_rich_mention_magazine_help: Bildigi revuan komponenton kiam revuo estas
menciita. Tio inkluzivos ĝian montritan nomon kaj bildsimbolon.
show_rich_ap_link: Riĉaj AP-ligiloj
show_rich_ap_link_help: Bildigi enlinian komponenton kiam alia
ActivityPub-enhavo estas ligita al.
attitude: Sinteno
type_search_magazine: Limigi serĉon al revuo...
type_search_user: Limigi serĉon al aŭtoro...
modlog_type_entry_deleted: Fadeno forigita
modlog_type_entry_restored: Fadeno restarigita
modlog_type_entry_comment_deleted: Komento pri fadeno forigita
modlog_type_entry_comment_restored: Fadenkomento restarigita
modlog_type_entry_pinned: Fadeno alpinglita
modlog_type_entry_unpinned: Fadeno malfiksita
modlog_type_post_deleted: Mikroblogo forigita
modlog_type_post_restored: Mikroblogo restarigita
modlog_type_post_comment_deleted: Respondo al mikroblogo forigita
modlog_type_post_comment_restored: Respondo al mikroblogo restarigita
modlog_type_ban: Uzanto malpermesita de revuo
modlog_type_moderator_add: Revumoderigisto aldonita
modlog_type_moderator_remove: Revuomoderigisto forigita
crosspost: Disafiŝi
ban_expires: Forbaro eksvalidiĝas
banner: Standardo
magazine_theme_appearance_banner: Propra standardo por la revuo. Ĝi estas
montrata super ĉiuj fadenoj kaj devus esti en larĝa bildformato (5:1, aŭ
1500px * 300px).
flash_thread_ref_image_not_found: La bildo referencia de 'imageHash' ne
trovitis.
everyone: Ĉiuj
nobody: Neniu
followers_only: Nur sekvantoj
direct_message_setting_label: Kiu povas sendi al vi rektmesaĝon
delete_magazine_icon: Forigi revuikonon
flash_magazine_theme_icon_detached_success: Revuikono sukcese forigitis
delete_magazine_banner: Forigi revustandardo
flash_magazine_theme_banner_detached_success: Revustandardo sukcese forigitis
federation_uses_allowlist: Uzi permesliston por federacio
defederating_instance: Malfederanta nodo %i
their_user_follows: Kvanto da uzantoj de ilia nodo sekvantaj uzantojn sur nia
nodo
our_user_follows: Kvanto da uzantoj de nia nodo sekvantaj uzantojn sur ilia nodo
their_magazine_subscriptions: Kvanto da uzantoj de ilia nodo abonis revuojn en
nia nodo
our_magazine_subscriptions: Kvanto da uzantoj de nia nodo abonis revuojn en ilia
nodo
confirm_defederation: Konfirmi malfederacion
ban_instance: Forbari nodon
allow_instance: Malforbari nodon
================================================
FILE: translations/messages.es.yaml
================================================
type.link: Enlace
type.article: Hilo
type.photo: Foto
type.video: Vídeo
type.smart_contract: Contrato inteligente
type.magazine: Revista
thread: Hilo
threads: Hilos
microblog: Microblog
people: Gente
events: Eventos
magazine: Revista
magazines: Revistas
search: Buscar
add: Añadir
select_channel: Elige un canal
login: Iniciar sesión
top: Destacado
hot: Popular
active: Activo
newest: Más reciente
oldest: Más antiguo
commented: Comentado
change_view: Cambiar vista
filter_by_time: Ordenar por orden cronológico
filter_by_type: Ordenar por tipo
comments_count: '{0}Comentarios|{1}Comentario|]1,Inf[ Comentarios'
favourites: Votos a favor
favourite: Favorito
more: Más
avatar: Avatar
added: Añadido
general: General
created_at: Creado
owner: Propietario/a
subscribers: Suscriptores/as
down_votes: Votos en contra
no_comments: No hay comentarios
online: En línea
comments: Comentarios
posts: Publicaciones
replies: Respuestas
moderators: Moderadores/as
mod_log: Registro de moderación
add_comment: Añadir comentario
add_post: Añadir publicación
add_media: Añadir medio
markdown_howto: ¿Cómo se utiliza el editor?
enter_your_comment: Escribe tu comentario
enter_your_post: Introduce tu publicación
activity: Actividad
cover: Portada
related_posts: Publicaciones relacionadas
random_posts: Publicaciones al azar
federated_magazine_info: Esta revista es de un servidor federado y podría estar
incompleta.
federated_user_info: Este perfil es de un servidor federado y podría estar
incompleto.
go_to_original_instance: Ver en instancia remota
empty: Vacío
subscribe: Suscribirse
unsubscribe: Cancelar suscripción
follow: Seguir
unfollow: Dejar de seguir
reply: Responder
login_or_email: Identificador o correo electrónico
password: Contraseña
remember_me: Recordarme
dont_have_account: ¿No tienes una cuenta?
you_cant_login: ¿Has olvidado tu contraseña?
register: Registrarse
reset_password: Restablecer la contraseña
show_more: Mostrar más
to: a
in: en
username: Nombre de usuarie
email: Correo electrónico
repeat_password: Repetir la contraseña
terms: Condiciones de uso
privacy_policy: Política de privacidad
about_instance: Acerca de
all_magazines: Todas las revistas
stats: Estadísticas
fediverse: Fediverso
create_new_magazine: Crear una nueva revista
add_new_article: Agregar un nuevo hilo
add_new_link: Añadir un nuevo enlace
add_new_photo: Añadir una nueva foto
add_new_post: Añadir una nueva publicación
add_new_video: Añadir un nuevo vídeo
contact: Contacto
faq: Preguntas frecuentes
rss: RSS
change_theme: Cambiar estilo
useful: Útil
help: Ayuda
check_email: Verifica tu correo electrónico
reset_check_email_desc: Si ya existe una cuenta asociada con tu correo
electrónico, recibirás un mensaje a la brevedad que contiene un enlace que
puedes usar para restablecer tu contraseña. Este enlace expirará en %expire%.
reset_check_email_desc2: Si no has recibido un correo electrónico, por favor
verifica tu carpeta de spam.
try_again: Intentar de nuevo
email_confirm_header: ¡Hola! Confirma tu dirección de correo electrónico.
email_confirm_content: '¿Listo para activar tu cuenta de Mbin? Haz clic en el siguiente
enlace:'
email_verify: Confirma la dirección de correo electrónico
email_confirm_expire: Ten en cuenta que el enlace caducará en una hora.
select_magazine: Selecciona una revista
add_new: Añadir nuevo
url: URL
title: Título
already_have_account: ¿Ya tienes una cuenta?
agree_terms: Consentimiento a los %terms_link_start%Términos y
Condiciones%terms_link_end% y a la %policy_link_start%Política de
Privacidad%policy_link_end%
email_confirm_title: Confirma tu dirección de correo electrónico.
1w: 1 sem.
down_vote: Votar en contra
body: Cuerpo
tags: Etiquetas
badges: Distintivos
is_adult: 18+ / Explícito
eng: ENG
oc: Cont. Orig.
image: Imagen
image_alt: Texto alternativo para la imagen
name: Nombre
description: Descripción
rules: Normas
domain: Dominio
followers: Seguidores
following: Siguiendo
subscriptions: Suscripciones
overview: Vista general
cards: Tarjetas
columns: Columnas
user: Usuarie
joined: Inscrito/a
moderated: Moderado/a
people_local: Local
people_federated: Federado
reputation_points: Puntos de reputación
related_tags: Etiquetas relacionadas
go_to_content: Ir al contenido
go_to_filters: Ir a los filtros
go_to_search: Ir a la búsqueda
subscribed: Suscrito
all: Todo
logout: Cerrar sesión
classic_view: Vista clásica
compact_view: Vista compacta
chat_view: Vista chat
tree_view: Vista de árbol
table_view: Vista en tabla
cards_view: Vista en cartas
3h: 3h
6h: 6h
12h: 12h
1d: 1 día
1y: 1 año
1m: 1 mes
links: Enlaces
articles: Hilos
photos: Fotos
videos: Vídeos
report: Reportar
share: Compartir
copy_url: Copiar URL de Mbin
copy_url_to_fediverse: Copiar URL original
share_on_fediverse: Compartir en el Fediverso
edit: Editar
are_you_sure: ¿Estás seguro/a?
moderate: Moderar
reason: Motivo
delete: Borrar
edit_post: Editar publicación
show_users_avatars: Mostrar el avatar de usuarie
yes: Sí
no: No
show_magazines_icons: Mostrar iconos de las revistas
show_thumbnails: Mostrar las miniaturas
rounded_edges: Bordes redondeados
restored_comment_by: ha restaurado el comentario de
removed_thread_by: ha eliminado un hilo de
restored_thread_by: ha restaurado el hilo de
removed_comment_by: ha borrado el comentario de
removed_post_by: ha borrado la publicación de
restored_post_by: ha restaurado la publicación de
he_banned: baneado/a
he_unbanned: desbaneado/a
read_all: Leer todo
show_all: Mostrar todo
flash_register_success: ¡Bienvenida/o a bordo! Tu cuenta ya está registrada. Un
último paso - consulta tu bandeja de entrada para recibir un enlace de
activación que dará vida a tu cuenta.
flash_thread_new_success: El hilo ha sido creado con éxito y ahora es visible
para otras/os usuarias/os.
flash_thread_edit_success: El hilo ha sido editado con éxito.
flash_thread_delete_success: El hilo ha sido borrado con éxito.
flash_thread_pin_success: El hilo ha sido anclado con éxito.
flash_thread_unpin_success: El hilo ha sido desanclado con éxito.
flash_magazine_new_success: La revista ha sido creado con éxito. Ahora puedes
añadir nuevo contenido o explorar el panel de administración.
flash_magazine_edit_success: La revista ha sido editado con éxito.
too_many_requests: Límite excedido, por favor, inténtalo de nuevo más tarde.
set_magazines_bar: Barra de revistas
set_magazines_bar_desc: añade el nombre de la revista tras la coma
set_magazines_bar_empty_desc: si el campo está vacío, las revistas activas se
mostrarán en la barra.
up_votes: Impulsos
edit_comment: Guardar cambios
settings: Configuración
profile: Perfil
blocked: Bloqueados
reports: Reportes
notifications: Notificaciones
messages: Mensajes
appearance: Apariencia
homepage: Página de inicio
hide_adult: Ocultar contenido explícito
featured_magazines: Revistas destacadas
privacy: Privacidad
show_profile_subscriptions: Mostrar suscripciones a revistas
show_profile_followings: Mostrar usuaries seguides
notify_on_new_entry_reply: Cualquier nivel de comentarios en los hilos que he
creado
notify_on_new_entry_comment_reply: Respuestas a mis comentarios en cualquier
hilo
notify_on_new_post_reply: Cualquier nivel de respuestas a las publicaciones que
he creado
notify_on_new_post_comment_reply: Respuestas a mis comentarios en cualquier
publicación
notify_on_new_entry: Nuevos hilos (enlaces o artículos) en cualquier revista a
la que me haya suscrito
notify_on_new_posts: Nuevas publicaciones en cualquier revista a la que me haya
suscrito
save: Guardar
about: Acerca de
old_email: Correo electrónico actual
new_email: Nuevo correo electrónico
new_email_repeat: Confirmar nuevo correo electrónico
current_password: Contraseña actual
new_password: Nueva contraseña
new_password_repeat: Confirmar la nueva contraseña
change_email: Cambiar correo electrónico
change_password: Cambiar contraseña
expand: Expandir
collapse: Plegar
domains: Dominios
error: Error
votes: Votos
theme: Aspecto
dark: Oscuro
light: Claro
solarized_light: Claro Solarizado
solarized_dark: Oscuro Solarizado
font_size: Tamaño de la fuente
size: Tamaño
up_vote: Impulsar
mod_log_alert: En el registro de moderación podrás encontrar contenido
desagradable u ofensivo eliminado por los/as moderadores/as. Asegúrate de
saber lo que estás haciendo.
added_new_thread: Agregado un nuevo hilo
edited_thread: Editado un hilo
mod_remove_your_thread: Un/a moderador/a ha borrado tu hilo
added_new_comment: Agregado nuevo comentario
edited_comment: Editado un comentario
replied_to_your_comment: Ha respondido a tu comentario
mod_deleted_your_comment: Un/a moderador/a ha borrado tu comentario
added_new_post: Agregada una nueva publicación
edited_post: Editada una publicación
mod_remove_your_post: Un/a moderador/a ha borrado tu publicación
added_new_reply: Agregada una nueva respuesta
wrote_message: Ha escrito un mensaje
banned: Te ha baneado
removed: Borrado por un/a moderador/a
deleted: Borrado por el/la autor/a
mentioned_you: Te ha mencionado
comment: Comentario
post: Publicación
ban_expired: Baneo expirado
purge: Purgar
send_message: Enviar un mensaje directo
message: Mensaje
infinite_scroll: Desplazamiento infinito
show_top_bar: Mostrar la barra superior
sticky_navbar: Barra de navegación fija
subject_reported: El contenido ha sido reportado.
sidebar_position: Posición de la barra lateral
left: Izquierda
right: Derecha
federation: Federación
status: Estado
on: Encendido
off: Apagado
instances: Instancias
upload_file: Cargar archivo
from_url: De una URL
magazine_panel: Panel de la revista
reject: Rechazar
approve: Aprobar
ban: Banear
filters: Filtros
approved: Aprobado
rejected: Rechazado
add_moderator: Añadir un/a moderador/a
add_badge: Añadir un distintivo
bans: Baneos
created: Creado
expires: Expira
perm: Permanente
expired_at: Expiró el
add_ban: Añadir baneo
trash: Papelera
icon: Icono
done: Hecho
unpin: Desanclar
pin: Anclar
change_magazine: Cambiar de revista
change_language: Cambiar idioma
change: Cambio
pinned: Anclado
preview: Vista previa
article: Hilo
reputation: Reputación
note: Nota
writing: Escritura
users: Usuarias/os
content: Contenido
week: Semana
weeks: Semanas
month: Mes
months: Meses
year: Año
federated: Federado
local: Local
admin_panel: Panel de administración
dashboard: Panel de control
contact_email: e-mail de contacto
meta: Meta
instance: Instancia
registrations_enabled: Registro activado
pages: Páginas
FAQ: FAQ
type_search_term: Escribir término de búsqueda
federation_enabled: Federación activada
registration_disabled: Inscripciones desactivadas
restore: Restaurar
add_mentions_entries: Añadir etiquetas de mención en los hilos
add_mentions_posts: Añadir etiquetas de mención en las publicaciones
Password is invalid: La contraseña no es válida.
Your account is not active: Tu cuenta no es activa.
Your account has been banned: Tu cuenta ha sido baneada.
firstname: Primer nombre
send: Enviar
active_users: Personas activas
random_entries: Hilos al azar
related_entries: Hilos relacionados
delete_account: Eliminar la cuenta
purge_account: Purgar la cuenta
ban_account: Banear la cuenta
unban_account: Desbanear la cuenta
related_magazines: Revistas relacionadas
random_magazines: Revistas al azar
magazine_panel_tags_info: Indícalo sólo si deseas que el contenido del fediverso
se incluya en esta revista en función de las etiquetas
sidebar: Barra lateral
auto_preview: Vista previa de medios
dynamic_lists: Listas dinámicas
banned_instances: Instancias bloqueadas
kbin_intro_title: Explorar el Fediverso
kbin_intro_desc: es una plataforma descentralizada de agregación de contenidos y
microblogging que opera dentro de la red Fediverso.
kbin_promo_title: Crea tu propia instancia
captcha_enabled: Captcha activado
header_logo: Logo del encabezado
browsing_one_thread: Estás viendo solo un hilo de la discusión! En la página de
la publicación puedes encontrar todos los comentarios.
return: Volver
kbin_promo_desc: '%link_start%Clona el repositorio%link_end% y desarrolla el Fediverso'
boosts: Impulsos
boost: Impulsar
mercure_enabled: Mercure habilitado
report_issue: Informar de un problema
tokyo_night: Noche de Tokio
preferred_languages: Filtrar idiomas de hilos y publicaciones
toolbar.bold: Negrita
toolbar.italic: Cursiva
toolbar.strikethrough: Tachado
toolbar.header: Encabezado
toolbar.quote: Cita
toolbar.code: Código
toolbar.link: Enlace
toolbar.image: Imagen
toolbar.unordered_list: Lista sin ordenar
toolbar.ordered_list: Lista ordenada
toolbar.mention: Mención
reload_to_apply: Recargar la página para aplicar los cambios
local_and_federated: Locales y federados
bot_body_content: "¡Bienvenido al Agente Mbin! Este agente juega un papel crucial
al habilitar la funcionalidad ActivityPub dentro de Mbin. Garantiza que Mbin pueda
comunicarse y federarse con otras instancias del fediverso.\n\nActivityPub es un
protocolo estándar abierto que permite que las plataformas de redes sociales descentralizadas
se comuniquen e interactúen entre sí. Permite a les usuaries en diferentes instancias
(servidores) seguir, interactuar y compartir contenido a través de la red social
federada conocida como fediverse. Proporciona una forma estandarizada para que los
usuarios publiquen contenido, sigan a otres usuaries y participen en interacciones
sociales como dar me gusta, compartir y comentar en hilos o publicaciones."
your_account_is_not_active: Su cuenta no ha sido activada. Consulte su correo
electrónico para obtener instrucciones sobre la activación de la cuenta o solicite un nuevo correo electrónico de activación de la
cuenta.
more_from_domain: Más del dominio
your_account_has_been_banned: Tu cuenta ha sido baneada
infinite_scroll_help: Cargar automáticamente más contenido cuando llegue al
final de la página.
sticky_navbar_help: La barra de navegación se pegará a la parte superior de la
página cuando se desplace hacia abajo.
auto_preview_help: Muestra la previsualización (foto, video) en tamaño grande
bajo el contenido.
filter.origin.label: Elegir origen
filter.fields.label: Elija los campos que desea buscar
filter.adult.label: Elija si desea mostrar NSFW
filter.adult.hide: Ocultar NSFW
filter.adult.show: Mostrar NSFW
filter.adult.only: Sólo NSFW
filter.fields.only_names: Sólo nombres
filter.fields.names_and_descriptions: Nombres y descripciones
kbin_bot: Agente Mbin
password_confirm_header: Confirme su solicitud de cambio de contraseña.
federation_page_enabled: Página de la Federación activada
federation_page_allowed_description: Instancias conocidas con las que nos
federamos
federation_page_disallowed_description: Instancias con las que no nos federamos
federated_search_only_loggedin: Búsqueda federada limitada si no se ha iniciado
sesión
email_confirm_button_text: Confirma tu solicitud de cambio de la contraseña
errors.server429.title: 429 Demasiadas solicitudes
errors.server404.title: 404 No encontrado
errors.server403.title: 403 Prohibido
errors.server500.title: 500 Error interno de servidor
errors.server500.description: Algo salió mal de nuestra parte. Si continúas
viendo este error, intenta comunicarse con el/a propietario/a de la instancia.
Si esta instancia no funciona en absoluto, ve a %link_start%otras instancias
de Mbin%link_end% mientras tanto hasta que se resuelva el problema.
email_confirm_link_help: También puedes copiar y pegar lo siguiente en el
navegador
resend_account_activation_email_error: Se ha producido un problema al enviar
esta solicitud. Es posible que no haya ninguna cuenta asociada a ese correo
electrónico o que ya esté activada.
email.delete.description: Este usuarie ha solicitado que se elimine su cuenta
custom_css: CSS personalizado
resend_account_activation_email_success: Si existe una cuenta asociada a ese
correo electrónico, enviaremos un nuevo mensaje de activación.
ignore_magazines_custom_css: Ignorar CSS personalizado de las revistas
oauth.consent.title: Formulario de consentimiento OAuth2
resend_account_activation_email: Volver a enviar el correo para la activación de
la cuenta
resend_account_activation_email_question: ¿Cuenta inactiva?
resend_account_activation_email_description: Introduce la dirección de correo
electrónico asociada a tu cuenta. Te enviaremos otro mensaje de activación.
oauth.consent.grant_permissions: Conceder los permisos
email.delete.title: Solicitud de eliminación de la cuenta
oauth2.grant.moderate.magazine.reports.all: Gestionar los informes en las
revistas bajo tu moderación.
oauth.consent.to_allow_access: Para permitir este acceso, haga clic en el botón
"Permitir"
oauth.consent.app_requesting_permissions: querría realizar las siguientes
acciones en tu nombre
oauth2.grant.moderate.magazine.reports.action: Aceptar o rechazar informes en
las revistas bajo tu moderación.
oauth.consent.allow: Permitir
block: Bloquear
oauth2.grant.moderate.magazine.list: Mostrar una lista de las revistas bajo tu
moderación.
oauth2.grant.moderate.magazine.reports.read: Mostrar informes en las revistas
bajo tu moderación.
oauth.consent.deny: Denegar
oauth.client_not_granted_message_read_permission: Esta aplicación no tiene
permiso para leer tus mensajes.
restrict_oauth_clients: Restringir la creación de clientes OAuth2 a
administradores/as
unblock: Desbloquear
oauth2.grant.moderate.magazine.ban.delete: Desbanear usuaries en las revistas
que moderas.
oauth.client_identifier.invalid: ¡ID para el cliente de OAuth no válido!
oauth.consent.app_has_permissions: ya puede realizar las siguientes acciones
mark_as_adult: Marcar como explícito
subscribers_count: '{0}Suscriptores|{1}Suscriptor|]1,Inf[ Suscriptores'
followers_count: '{0}Seguidores|{1}Seguidor|]1,Inf[ Seguidores'
remove_media: Eliminar medio
menu: Menú
flash_mark_as_adult_success: La publicación se ha marcado correctamente como
explícita.
unmark_as_adult: Desmarcar como explícito
flash_unmark_as_adult_success: La publicación se ha desmarcado correctamente
como explícita.
default_theme: Tema por defecto
oauth2.grant.moderate.magazine_admin.delete: Eliminar todas las revistas de tu
propiedad.
oauth2.grant.moderate.magazine_admin.all: Crear, editar o eliminar las revistas
de tu propiedad.
oauth2.grant.moderate.magazine_admin.create: Crear nuevas revistas.
oauth2.grant.moderate.magazine_admin.update: Editar las reglas, la descripción,
el modo explícito o el ícono de cualquiera de tus revistas.
oauth2.grant.admin.all: Realizar cualquier acción administrativas en tu
instancia.
oauth2.grant.moderate.magazine_admin.badges: Crear o eliminar distintivos de las
revistas de tu propiedad.
oauth2.grant.moderate.magazine_admin.tags: Crear o eliminar etiquetas en las
revistas de tu propiedad.
oauth2.grant.moderate.magazine_admin.stats: Mostrar el contenido, las votaciones
y el estado de visualización de las revistas de tu propiedad.
oauth2.grant.domain.all: Suscríbirse o bloquear dominios y ver los dominios a
los que se suscribe o bloquea.
oauth2.grant.block.general: Bloquear o desbloquear cualquier revista, dominio o
usuarie, y ver las revistas, dominios y usuaries que ha bloqueado.
oauth2.grant.domain.subscribe: Suscríbirse o cancelar la suscripción a dominios
y ver los dominios a los que está suscrito.
oauth2.grant.domain.block: Bloquear o desbloquear dominios y ver los dominios
que ha bloqueado.
oauth2.grant.subscribe.general: Suscribirse o seguir cualquier revista, dominio
o usuarie, y ver las revistas, dominios y usuaries a los que se suscribe.
disconnected_magazine_info: Esta revista no recibe actualizaciones (última
actividad hace %days% día(s)).
always_disconnected_magazine_info: Esta revista no recibe actualizaciones.
subscribe_for_updates: Suscríbete para comenzar a recibir actualizaciones.
default_theme_auto: Claro/Oscuro (detección automática)
solarized_auto: Solarizado (Detección automática)
oauth2.grant.moderate.magazine.trash.read: Mostrar el contenido de la papelera
en tus revistas moderadas.
oauth2.grant.moderate.magazine_admin.edit_theme: Editar el CSS personalizado de
tus revistas.
oauth2.grant.moderate.magazine_admin.moderators: Añadir o eliminar
moderadores/as a las revistas de tu propiedad.
oauth2.grant.admin.entry.purge: Eliminar completamente cualquier hilo de tu
instancia.
oauth2.grant.read.general: Leer todo el contenido al que tienes acceso.
oauth2.grant.write.general: Crear o editar cualquiera de tus hilos,
publicaciones o comentarios.
oauth2.grant.delete.general: Eliminar cualquiera de tus hilos, publicaciones o
comentarios.
oauth2.grant.report.general: Reportar hilos, publicaciones o comentarios.
oauth2.grant.vote.general: Votar a favor, en contra o impulsar hilos,
publicaciones o comentarios.
marked_for_deletion: Marcado para eliminar
from: desde
tag: Etiqueta
unban: Desbanear
ban_hashtag_btn: Prohibir hashtag
marked_for_deletion_at: Marcado para eliminar el %date%
sort_by: Ordenar por
filter_by_subscription: Filtrar por suscripción
filter_by_federation: Filtrar por estado de federación
hidden: Oculto
disabled: Desactivado
edit_entry: Editar hilo
oauth2.grant.entry_comment.create: Crear nuevos comentarios en hilos.
oauth2.grant.entry_comment.delete: Eliminar todos tus comentarios existentes en
los hilos.
enabled: Activado
downvotes_mode: Modo de votos negativos
unban_hashtag_description: Si retiras la prohibición de un hashtag, se podrán
volver a crear publicaciones con este hashtag. Las publicaciones existentes
con este hashtag ya no estarán ocultas.
change_downvotes_mode: Cambiar el modo de votos negativos
oauth2.grant.magazine.all: Suscríbirse o bloquear revistas y visualizar las
revistas a las que estás suscrito o has bloqueado.
oauth2.grant.magazine.subscribe: Suscríbirse o cancelar tu suscripción a
revistas y consultar las revistas a las que te suscribes.
oauth2.grant.post.report: Reportar cualquier publicación.
unban_hashtag_btn: Retirar la prohibición de hashtag
ban_hashtag_description: Prohibir un hashtag impedirá que se creen publicaciones
con ese hashtag, además de ocultar las publicaciones existentes con ese
hashtag.
account_deletion_immediate: Eliminar inmediatamente
account_deletion_button: Eliminar cuenta
private_instance: Obligar a les usuaries a iniciar sesión antes de poder acceder
a cualquier contenido
oauth2.grant.entry.edit: Editar tus hilos existentes.
oauth2.grant.entry.report: Reportar cualquier hilo.
oauth2.grant.entry_comment.all: Crear, editar o eliminar tus comentarios en
hilos, y votar, impulsar o denunciar cualquier comentario en un hilo.
oauth2.grant.entry_comment.vote: Votar a favor, impulsar o rechazar cualquier
comentario en un hilo.
oauth2.grant.magazine.block: Bloquear o desbloquear revistas y visualizar las
revistas que has bloqueado.
oauth2.grant.post.all: Crear, editar o eliminar tus microblogs y votar, impulsar
o reportar cualquier microblog.
oauth2.grant.post.create: Crear nuevas publicaciones.
oauth2.grant.post_comment.create: Crear nuevos comentarios en las publicaciones.
oauth2.grant.post_comment.delete: Eliminar todos tus comentarios en las
publicaciones.
oauth2.grant.post_comment.edit: Editar todos tus comentarios en las
publicaciones.
oauth2.grant.entry.create: Crear nuevos hilos.
oauth2.grant.entry.vote: Votar a favor, impulsar o rechazar cualquier hilo.
oauth2.grant.entry.delete: Eliminar tus hilos.
oauth2.grant.entry_comment.edit: Editar tus comentarios existentes en los hilos.
oauth2.grant.post.vote: Votar a favor o en contra, o impulsar cualquier
publicación.
oauth2.grant.entry.all: Crear, editar o eliminar tus hilos y votar, impulsar o
reportar cualquier hilo.
account_deletion_title: Borrado de cuenta
oauth2.grant.post.edit: Editar todas tus publicaciones.
oauth2.grant.post.delete: Eliminar todas tus publicaciones.
oauth2.grant.post_comment.all: Crear, editar o eliminar tus comentarios en las
publicaciones y votar, impulsar o reportar cualquier comentario en una
publicación.
oauth2.grant.entry_comment.report: Reportar cualquier comentario en un hilo.
federation_page_dead_description: Casos en los que no pudimos realizar al menos
10 actividades seguidas y donde la última entrega y recepción exitosas fueron
hace más de una semana
account_deletion_description: Tu cuenta se eliminará en 30 días a menos que
elijas eliminarla inmediatamente. Para recuperar tu cuenta en un plazo de 30
días, inicia sesión con las mismas credenciales o comunícate con
administración.
oauth2.grant.user.oauth_clients.read: Leer los permisos que ha concedido a otras
aplicaciones OAuth2.
oauth2.grant.moderate.post.trash: Eliminar o restaurar publicaciones en las
revistas que moderas.
moderation.report.ban_user_title: Banear cuenta
oauth2.grant.user.oauth_clients.all: Leer y editar los permisos que has otorgado
a otras aplicaciones OAuth2.
oauth2.grant.moderate.entry.change_language: Cambiar el idioma de los hilos en
las revistas que moderas.
oauth2.grant.moderate.entry.pin: Fijar hilos en la parte superior de las
revistas que moderas.
oauth2.grant.moderate.entry.set_adult: Marcar los hilos como explícitos en las
revistas que moderas.
oauth2.grant.admin.oauth_clients.read: Vernlos clientes OAuth2 que existen en tu
instancia y sus estadísticas de uso.
last_active: Última actividad
oauth2.grant.admin.federation.update: Añadir o eliminar instancias de la lista
de instancias desfederadas.
oauth2.grant.moderate.entry.all: Moderar los hilos en las revistas que moderas.
oauth2.grant.admin.magazine.purge: Borrar completamente las revistas de tu
instancia.
oauth2.grant.post_comment.vote: Votar a favor, en contra o impulsar cualquier
comentario de una publicación .
oauth2.grant.user.profile.all: Leer y editar tu perfil.
oauth2.grant.user.profile.read: Leer tu perfil.
oauth2.grant.moderate.post_comment.all: Moderar los comentarios en las
publicaciones de las revistas que moderas.
oauth2.grant.admin.magazine.move_entry: Mover los hilos entre las revistas de tu
instancia.
oauth2.grant.admin.user.ban: Banear o desbanear cuentas de tu instancia.
oauth2.grant.admin.user.delete: Eliminar cuentas de tu instancia.
oauth2.grant.admin.instance.settings.read: Ver la configuración de tu instancia.
oauth2.grant.admin.instance.settings.edit: Actualizar la configuración de tu
instancia.
show_avatars_on_comments_help: Mostrar u ocultar los avatares de los usuaries al
ver comentarios en un solo hilo o publicación .
comment_reply_position: Posición del comentario de respuesta
magazine_theme_appearance_icon: Icono personalizado para la revista.
moderation.report.approve_report_title: Aprobar informe
moderation.report.ban_user_description: ¿Quieres banear a (%username%) que creó
este contenido de esta revista?
moderation.report.approve_report_confirmation: ¿De verdad quieres aprobar este
informe?
subject_reported_exists: Este contenido ya ha sido reportado.
oauth2.grant.admin.federation.read: Ver la lista de instancias desfederadas.
oauth2.grant.user.block: Bloquear o desbloquear cuentas, y leer una lista de las
que bloqueas.
oauth2.grant.moderate.all: Realizar cualquier acción de moderación que tengas
permiso para hacer en las revistas que moderas.
oauth2.grant.admin.instance.all: Ver y actualizar la configuración o la
información de la instancia.
single_settings: Único
moderation.report.reject_report_title: Rechazar informe
oauth2.grant.moderate.magazine.ban.create: Banear cuentas en las revistas que
moderas.
oauth2.grant.admin.user.purge: Eliminar completamente cuentas de tu instancia.
oauth2.grant.admin.oauth_clients.all: Ver o revocar clientes OAuth2 que existen
en tu instancia.
flash_post_pin_success: La publicación se ha anclado correctamente.
flash_post_unpin_success: La publicación se ha desanclado correctamente.
oauth2.grant.user.profile.edit: Editar tu perfil.
oauth2.grant.moderate.entry_comment.all: Moderar los comentarios en los hilos de
las revistas que moderas.
oauth2.grant.moderate.entry.trash: Eliminar o restaurar hilos en las revistas
que moderas.
oauth2.grant.admin.instance.information.edit: Actualizar las páginas Acerca de,
Preguntas frecuentes, Contacto, Condiciones del servicio y Política de
privacidad de tu instancia.
update_comment: Actualizar comentario
oauth2.grant.admin.post.purge: Borrar completamente cualquier mensaje de tu
instancia.
oauth2.grant.admin.post_comment.purge: Eliminar completamente cualquier
comentario de una entrada de tu instancia.
oauth2.grant.user.all: Leer y editar tu perfil, mensajes o notificaciones; leer
y editar los permisos que has otorgado a otras aplicaciones; seguir o bloquear
a usuaries; ver listas de usuaries que sigues o bloqueas.
oauth2.grant.user.follow: Seguir o dejar de seguir cuentas, y leer la lista de
las que sigues.
oauth2.grant.moderate.post_comment.change_language: Cambiar el idioma de los
comentarios en las publicaciones de las revistas que moderas.
magazine_theme_appearance_custom_css: CSS personalizado que se aplicará al
visualizar el contenido dentro de su revista.
oauth2.grant.admin.entry_comment.purge: Eliminar por completo cualquier
comentario en un hilo de tu instancia.
oauth2.grant.moderate.magazine.ban.all: Gestionar cuentas baneadas en las
revistas que moderas.
oauth2.grant.moderate.magazine.ban.read: Ver cuentas baneadas en las revistas
que moderas.
oauth2.grant.admin.federation.all: Ver y actualizar las instancias actualmente
desfederadas.
oauth2.grant.admin.instance.settings.all: Ver o actualizar la configuración de
tu instancia.
oauth2.grant.moderate.post.change_language: Cambiar el idioma de las
publicaciones en las revistas que moderas.
oauth2.grant.admin.instance.stats: Consultar las estadísticas de tu instancia.
oauth2.grant.admin.user.all: Banear, verificar o eliminar completamente usuaries
de tu instancia.
oauth2.grant.post_comment.report: Reportar cualquier comentario en una
publicación.
magazine_theme_appearance_background_image: Imagen de fondo personalizada que se
aplicará al visualizar el contenido dentro de su revista.
oauth2.grant.moderate.post_comment.set_adult: Marcar como explícitos los
comentarios en las publicaciones en las revistas que moderas.
oauth2.grant.user.notification.delete: Eliminar tus notificaciones.
oauth2.grant.moderate.magazine.all: Administrar prohibiciones, informes y
visualizar elementos eliminados en las revistas que moderas.
oauth2.grant.admin.magazine.all: Mover hilos entre o eliminar completamente
revistas en tu instancia.
oauth2.grant.user.oauth_clients.edit: Editar los permisos que has concedido a
otras aplicaciones OAuth2.
oauth2.grant.moderate.post.set_adult: Marca como explícitas las publicaciones en
las revistas que moderas.
oauth2.grant.admin.oauth_clients.revoke: Revocar el acceso a clientes OAuth2 en
tu instancia.
comment_reply_position_help: Mostrar el formulario de respuesta a los
comentarios en la parte superior o inferior de la página. Cuando está
habilitado el "desplazamiento infinito", la posición siempre aparecerá en la
parte superior.
oauth2.grant.moderate.entry_comment.set_adult: Marcar como explícitos los
comentarios en los hilos en las revistas que moderas.
oauth2.grant.moderate.entry_comment.trash: Eliminar o restaurar comentarios en
hilos de las revistas que moderas.
oauth2.grant.moderate.post.all: Moderar las publicaciones en las revistas que
moderas.
oauth2.grant.moderate.entry_comment.change_language: Cambiar el idioma de los
comentarios en los hilos de las revistas que moderas.
oauth2.grant.moderate.post_comment.trash: Eliminar o restaurar comentarios en
publicaciones de las revistas que moderas.
oauth2.grant.admin.user.verify: Verificar usuaries en tu instancia.
oauth2.grant.user.notification.all: Leer y eliminar tus notificaciones.
oauth2.grant.user.notification.read: Leer tus notificaciones, incluidas las de
mensajes.
oauth2.grant.user.message.all: Leer tus mensajes y enviar mensajes a otres
usuaries.
oauth2.grant.user.message.read: Leer tus mensajes.
oauth2.grant.user.message.create: Enviar mensajes a usuaries.
flash_image_download_too_large_error: No se ha podido crear la imagen, es
demasiado grande (tamaño máximo %bytes%)
pending: Pendiente
announcement: Anuncio
2fa.backup_codes.recommendation: Guarda una copia de los mismos en un lugar
seguro.
2fa.available_apps: Usar una aplicación para la autenticación de dos factores
como %google_authenticator%, %aegis% (Android) o %raivo% (iOS) para escanear
el código QR.
2fa.backup_codes.help: Puedes usar estos códigos cuando no tengas tu dispositivo
o aplicación de autenticación de dos factores. No se volverán a
mostrar y podrás utilizar cada uno de ellos solo una
vez .
show_subscriptions: Mostrar suscripciones
subscription_sidebar_pop_out_right: Mover a una barra lateral separada a la
derecha
subscription_sidebar_pop_out_left: Mover a la barra lateral separada a la
izquierda
subscription_sidebar_pop_in: Mover suscripciones al panel emergente
subscriptions_in_own_sidebar: En una barra lateral separada
flash_thread_tag_banned_error: No se pudo crear el hilo. El contenido no está
permitido.
flash_email_failed_to_sent: No se ha podido enviar el correo electrónico.
flash_post_new_success: La publicación se ha creado correctamente.
flash_post_new_error: No se pudo crear la publicación. Algo salió mal.
flash_magazine_theme_changed_success: Se ha actualizado la apariencia de la
revista.
flash_user_edit_password_error: Error al cambiar la contraseña.
ownership_requests: Solicitudes de titularidad
flash_email_was_sent: El correo electrónico se ha enviado correctamente.
purge_content: Purgar contenido
purge_content_desc: Purgar completamente el contenido de usuarie, incluidas las
respuestas de otres usuaries en conversaciones, publicaciones y comentarios
que has creado.
delete_account_desc: Eliminar la cuenta, incluidas las respuestas de otres
usuaries en hilos, publicaciones y comentarios que has creado.
schedule_delete_account: Programar eliminación
remove_schedule_delete_account: Cancelar la eliminación programada
remove_schedule_delete_account_desc: Cancelar la eliminación programada. Todo el
contenido volverá a estar disponible y el usuarie podrá iniciar sesión.
two_factor_authentication: Autenticación de dos factores
2fa.setup_error: Error al habilitar la autenticación de dos factores para la
cuenta
2fa.enable: Configurar la autenticación de dos factores
2fa.code_invalid: El código de autenticación no es válido
2fa.disable: Desactivar la autenticación de dos factores
2fa.backup: Tus códigos de respaldo para la autenticación de dos factores
2fa.backup-create.help: Puedes crear nuevos códigos de autenticación de
respaldo; al hacerlo, se invalidarán los códigos existentes.
2fa.add: Añadir a mi cuenta
2fa.qr_code_img.alt: Un código QR que permite configurar la autenticación de dos
factores para tu cuenta
sidebars_same_side: Barras laterales en el mismo lado
flash_user_settings_general_success: La configuración se guardó correctamente.
account_is_suspended: Cuenta suspendida.
account_unbanned: Se ha desbaneado la cuenta.
apply_for_moderator: Solicitar ser moderador/a
cancel_request: Cancelar solicitud
remove_subscriptions: Eliminar suscripciones
abandoned: Abandonado
moderator_requests: Solicitudes de moderación
user_badge_global_moderator: Moderador global
user_badge_admin: Administrador/a
user_badge_moderator: Moderador/a
user_badge_bot: Bot
keywords: Palabras clave
sensitive_warning: Contenido sensible
sensitive_toggle: Alternar la visibilidad del contenido sensible
deleted_by_author: El hilo, mensaje o comentario ha sido eliminado por su
autor/a
all_time: Todo el tiempo
toolbar.spoiler: Destripe
alphabetically: Alfabéticamente
subscription_panel_large: Panel grande
close: Cerrar
flash_magazine_theme_changed_error: No se pudo actualizar la apariencia de la
revista.
flash_comment_new_success: El comentario ha sido creado correctamente.
flash_comment_new_error: No se pudo crear el comentario. Algo salió mal.
flash_user_settings_general_error: No se pudo guardar la configuración.
details: Detalles
flash_user_edit_profile_error: No se pudo guardar la configuración del perfil.
flash_thread_edit_error: No se pudo editar el hilo. Algo ha salido mal.
2fa.remove: Eliminar la autenticación de dos factores
2fa.backup-create.label: Crear nuevos códigos de autenticación de respaldo
2fa.verify_authentication_code.label: Ingresa un código del doble factor para
verificar la configuración
subscription_sort: Ordenar
subscription_header: Revistas suscritas
sensitive_hide: Pulse para ocultar
2fa.verify: Verificar
2fa.user_active_tfa.title: La cuenta tiene activa la autenticación de dos
factores
cancel: Cancelar
oauth2.grant.moderate.post.pin: Fijar publicaciones en la parte superior de las
revistas que moderas.
moderation.report.reject_report_confirmation: ¿De verdad quiere rechazar este
informe?
show_avatars_on_comments: Mostrar avatares en los comentarios
2fa.qr_code_link.title: Vsitar este enlace permite a tu plataforma registrar
esta autenticación de dos factores
delete_content_desc: Eliminar el contenido de usuarie pero dejar las respuestas
de otres usuaries en las conversaciones, publicaciones y comentarios.
schedule_delete_account_desc: Programar la eliminación de esta cuenta en 30
días. Esto ocultará al usuarie y su contenido, e impedirá que inicie sesión.
password_and_2fa: Contraseña y A2F
sensitive_show: Pulse para ver
action: Acción
user_badge_op: OP
deleted_by_moderator: Tema, mensaje o comentario eliminado por la moderación
spoiler: Spoiler
show: Mostrar
delete_content: Eliminar contenido
flash_comment_edit_error: No se pudo editar el comentario. Algo salió mal.
request_magazine_ownership: Pedir titularidad de revista
two_factor_backup: Códigos de respaldo de la autenticación de dos factores
flash_account_settings_changed: La configuración de tu cuenta se ha cambiado
correctamente. Tendrás que volver a iniciar sesión.
position_bottom: Inferior
position_top: Superior
flash_thread_new_error: No se pudo crear el hilo. Algo salió mal.
2fa.authentication_code.label: Código de autenticación
flash_comment_edit_success: El comentario se ha actualizado correctamente.
flash_user_edit_profile_success: La configuración del perfil se guardó
correctamente.
flash_user_edit_email_error: Error al cambiar el correo electrónico.
hide: Ocultar
edited: editado
sso_registrations_enabled: Registros SSO habilitados
sso_registrations_enabled.error: Los registros de nuevas cuentas con
administradores de identidad de terceros están actualmente deshabilitados.
accept: Aceptar
oauth2.grant.user.bookmark_list.delete: Borrar tus listas de marcadores
page_width_auto: Automático
auto: Automático
open_url_to_fediverse: Abrir URL original
change_my_avatar: Cambiar mi avatar
change_my_cover: Cambiar mi portada
account_settings_changed: Los ajustes de tu cuenta se han cambiado
correctamente. Necesitarás conectarte de nuevo.
magazine_deletion: Borrado de revista
suspend_account: Suspender cuenta
viewing_one_signup_request: Sólo estás viendo una solicitud de registro de
%username%
account_suspended: La cuenta ha sido suspendida.
oauth2.grant.user.bookmark.add: Añadir marcadores
oauth2.grant.user.bookmark: Añadir y eliminar marcadores
oauth2.grant.user.bookmark_list: Leer, editar y borrar tus listas de marcadores
oauth2.grant.user.bookmark_list.edit: Editar tus listas de marcadores
restore_magazine: Recuperar revista
unsuspend_account: Reactivar cuenta
deletion: Borrado
user_suspend_desc: Suspender tu cuenta oculta tu contenido en la instancia, pero
no lo borra permanentemente, y puedes restaurarlo en cualquier momento.
notify_on_user_signup: Nuevos registros
your_account_is_not_yet_approved: Tu cuenta no ha sido aprobada todavía. Te
enviaremos un email tan pronto como los administradores hayan procesado tu
solicitud de registro.
account_unsuspended: La cuenta ha sido reactivada.
edit_my_profile: Editar mi perfil
page_width_max: Máximo
page_width: Ancho de página
page_width_fixed: Fijo
flash_post_edit_error: Error al editar el post. Algo ha salido mal.
oauth2.grant.user.bookmark.remove: Eliminar marcadores
oauth2.grant.user.bookmark_list.read: Leer tus listas de marcadores
delete_magazine: Borrar revista
magazine_is_deleted: La revista ha sido borrada. Puedes restaurarla durante los próximos 30 días.
federation_page_dead_title: Instancias muertas
flash_post_edit_success: El post se ha editado correctamente.
filter_labels: Filtrar etiquetas
notification_title_new_post: Nueva publicación
notification_title_new_report: Se creó un nuevo informe
version: Versión
new_user_description: Esta cuenta es nueva (activa durante menos de %days% días)
new_magazine_description: Esta revista es nueva (activa durante menos de %days%
días)
admin_users_inactive: Inactivos/as
admin_users_active: Activos/as
remove_user_avatar: Eliminar avatar
account_banned: Se ha baneado la cuenta.
sso_only_mode: Restringir el inicio de sesión y el registro únicamente a métodos
SSO
related_entry: Relacionado
reporting_user: Denunciante
reported: denunciado
own_content_reported_accepted: Se aceptó un informe de tu contenido.
report_accepted: Se ha aceptado un informe
cake_day: Desde el día
magazine_log_entry_unpinned: se eliminó la entrada fijada
unregister_push_notifications_button: Eliminar registro "push"
test_push_notifications_button: Probar notificaciones "push"
notification_title_new_comment: Nuevo comentario
notification_title_removed_comment: Se eliminó un comentario
notification_title_edited_comment: Se editó un comentario
notification_title_message: Nuevo mensaje directo
notification_title_edited_post: Se editó una publicación
notification_title_new_signup: Se ha registrado una nueva cuenta
notification_body_new_signup: Se ha registrado la cuenta %u%.
last_successful_receive: Última recepción correcta
magazine_posting_restricted_to_mods: Restringir la creación de hilos a la
moderación
max_image_size: Tamaño máximo del archivo
bookmark_add_to_list: Añadir marcador a %list%
bookmark_remove_from_list: Eliminar el marcador de %list%
bookmark_list_create_placeholder: escribe el nombre...
table_of_contents: Tabla de contenido
search_type_all: Todo
search_type_entry: Hilos
application_text: Explica por qué quieres unirte
signup_requests_header: Solicitudes de registro
signup_requests_paragraph: Estes usuaries desean unirse a tu servidor. No podrán
iniciar sesión hasta que apruebes su solicitud de registro.
email_application_approved_title: Tu solicitud de registro ha sido aprobada
email_application_approved_body: La administración del servidor ha aprobado su
solicitud de registro. Ahora puede iniciar sesión en el servidor en %siteName% .
email_application_rejected_body: Gracias por su interés, pero lamentamos
informarle que su solicitud de registro ha sido rechazada.
show_user_domains: Mostrar dominios de usuarie
answered: contestado/a
by: por
front_default_sort: Orden predeterminado de la portada
comment_default_sort: Orden predeterminado de los comentarios
open_signup_request: Abrir solicitud de registro
show_thumbnails_help: Mostrar las miniaturas de las imágenes.
image_lightbox_in_list_help: Cuando está marcado, al hacer clic en la miniatura
se muestra una ventana modal con la imagen. Cuando no esté marcado, hacer clic
en la miniatura abrirá el hilo.
show_new_icons: Mostrar nuevos iconos
show_new_icons_help: Mostrar icono para nueva revista o cuenta (de 30 días de
antigüedad o más reciente)
user_verify: Activar cuenta
show_users_avatars_help: Mostrar la imagen del avatar del usuarie.
show_magazines_icons_help: Mostrar el icono de la revista.
test_push_message: ¡Hola mundo!
show_related_entries: Mostrar hilos al azar
search_type_post: Microblogs
select_user: Elige un usuarie
signup_requests: Solicitudes de registro
direct_message: Mensaje directo
remove_following: Eliminar el seguimiento
restrict_magazine_creation: Restringir la creación de revistas locales a
administradores y moderadores globales
reported_user: Cuenta reportada
own_report_rejected: Tu informe ha sido rechazado
back: Atrás
magazine_log_mod_added: Ha añadido un/a moderador/a
magazine_log_mod_removed: ha eliminado un/a moderador/a
magazine_log_entry_pinned: entrada fijada
someone: Alguien
notification_title_mention: Te mencionaron
notification_title_new_reply: Nueva respuesta
notification_title_new_thread: Nuevo hilo
notification_title_ban: Te banearon
notification_title_removed_thread: Se eliminó un hilo
notification_title_edited_thread: Se editó un hilo
notification_body2_new_signup_approval: Debes aprobar la solicitud antes de que
puedan iniciar sesión
show_related_posts: Mostrar publicaciones al azar
show_active_users: Mostrar cuentas activas
server_software: Software del servidor
last_successful_deliver: Última entrega correcta
bookmark_add_to_default_list: Añadir marcador a la lista predeterminada
bookmark_lists: Listas de marcadores
bookmark_remove_all: Eliminar todos los marcadores
bookmarks: Marcadores
is_default: Es predeterminada
bookmark_list_is_default: Es la lista predeterminada
bookmark_list_make_default: Hacer predeterminada
bookmark_list_create: Crear
bookmark_list_edit: Editar
bookmark_list_selected_list: Lista seleccionada
show_magazine_domains: Mostrar dominios de revistas
flash_application_info: La administración debe aprobar su cuenta antes de poder
iniciar sesión. Recibirá un correo electrónico una vez procesada su solicitud
de registro.
own_report_accepted: Tu informe ha sido aceptado
and: Y
last_updated: Última actualización
report_subject: Asunto
count: Recuento
bookmarks_list: Marcadores en %list%
email_application_rejected_title: Tu solicitud de registro ha sido rechazada
email_verification_pending: Debes verificar tu correo electrónico antes de poder
iniciar sesión.
comment_not_found: Comentario no encontrado
flash_posting_restricted_error: La creación de hilos está restringida a la
moderación de esta revista y no es parte de ella
last_failed_contact: Último contacto fallido
remove_user_cover: Eliminar portada
bookmarks_list_edit: Editar la lista de marcadores
manually_approves_followers: Aprueba seguidores manualmente
new_users_need_approval: Las nuevas cuentas deben ser aprobadas antes de poder
iniciar sesión.
register_push_notifications_button: Regístrese para recibir notificaciones
"push"
email_application_pending: Su cuenta requiere la aprobación de la administración
antes de poder iniciar sesión.
magazine_posting_restricted_to_mods_warning: Sólo la moderación puede crear
hilos en esta revista
compact_view_help: Una vista compacta con márgenes menores, donde la miniatura
pasa al lado derecho.
bookmark_list_create_label: Nombre de la lista
purge_magazine: Purgar revista
image_lightbox_in_list: Las miniaturas de los hilos abren pantalla completa
sso_show_first: Mostrar SSO primero en las páginas de inicio de sesión y
registro
continue_with: Continuar con
notification_title_removed_post: Se eliminó una publicación
show_related_magazines: Mostrar revistas al azar
admin_users_suspended: Suspendidos/as
admin_users_banned: Baneados/as
open_report: Abrir informe
toolbar.emoji: Emoji
2fa.manual_code_hint: Si no puedes escanear el código QR escribe el secreto
manualmente
magazine_instance_defederated_info: La instancia de esta revista está
desfederada. Por lo tanto, no recibirá actualizaciones.
user_instance_defederated_info: La instancia de esta cuenta está defederada.
flash_thread_instance_banned: La instancia de esta revista está baneada.
show_rich_mention: Menciones enriquecidas
show_rich_mention_help: Mostrar un componente de cuenta al mencionar a una
cuenta. Este incluirá su nombre para mostrar y su foto de perfil.
show_rich_mention_magazine: Menciones enriquecidas de revistas
show_rich_mention_magazine_help: Mostrar un componente de revista al mencionar
una revista. Esto incluirá su nombre para mostrar y su icono.
show_rich_ap_link: Enlaces AP enriquecidos
show_rich_ap_link_help: Mostrar un componente en línea cuando otro contenido de
ActivityPub está vinculado a él.
type_search_term_url_handle: Escribe el término de búsqueda, URL o identificador
search_type_magazine: Revistas
search_type_user: Cuentas
search_type_actors: Revistas + Cuentas
search_type_content: Temas + Microblogs
attitude: Actitud
type_search_magazine: Limitar la búsqueda a la revista...
type_search_user: Limitar búsqueda a la autoría...
modlog_type_entry_deleted: Hilo eliminado
modlog_type_entry_restored: Hilo restaurado
modlog_type_entry_comment_deleted: Comentario del hilo eliminado
modlog_type_entry_comment_restored: Comentario del hilo restaurado
modlog_type_entry_pinned: Hilo fijado
modlog_type_entry_unpinned: Hilo desfijado
modlog_type_post_deleted: Microblog eliminado
modlog_type_post_restored: Microblog restaurado
modlog_type_post_comment_deleted: Respuesta de microblog eliminada
modlog_type_post_comment_restored: Respuesta del microblog restaurada
modlog_type_ban: Cuenta baneada de la revista
modlog_type_moderator_add: Se agregó un/a moderador/a de la revista
modlog_type_moderator_remove: Moderador/a de la revista eliminado/a
created_since: Creado desde
crosspost: Publicación cruzada
ban_expires: La prohibición caduca
banner: Báner
oauth2.grant.moderate.entry.lock: Bloquea los hilos en las revistas que moderas,
para que nadie pueda comentarlos
oauth2.grant.moderate.post.lock: Bloquea los microblogs en las revistas que
moderas, para que nadie pueda comentarlos
magazine_theme_appearance_banner: Báner personalizado para la revista. Se
muestra sobre todos los hilos y debe tener una relación de aspecto amplia (5:1
o 1500 px x 300 px).
flash_thread_ref_image_not_found: No se pudo encontrar la imagen referenciada
por 'imageHash'.
everyone: Todo el mundo
nobody: Nadie
followers_only: Solo seguidores/as
direct_message_setting_label: Quién puede enviarte un mensaje directo
delete_magazine_icon: Eliminar el icono de la revista
flash_magazine_theme_icon_detached_success: El icono de la revista se eliminó
correctamente
delete_magazine_banner: Suprimir el báner de la revista
flash_magazine_theme_banner_detached_success: El báner de la revista se eliminó
correctamente
federation_uses_allowlist: Utilizar lista de permitidos para la federación
defederating_instance: Desfederando la instancia %i
their_user_follows: Cantidad de usuaries de su instancia que siguen a usuaries
de la nuestra
our_user_follows: Cantidad de usuaries de nuestra instancia que siguen a
usuaries en la suya
their_magazine_subscriptions: Cantidad de usuaries de su instancia suscrites a
revistas en la nuestra
our_magazine_subscriptions: Cantidad de usuaries de nuestra instancia suscrites
a revistas de la suya
confirm_defederation: Confirmar la desfederación
flash_error_defederation_must_confirm: Tienes que confirmar la desfederación
allowed_instances: Instancias permitidas
btn_deny: Denegar
================================================
FILE: translations/messages.et.yaml
================================================
type.link: Link
type.article: Jutulõng
type.photo: Foto
type.video: Video
type.smart_contract: Nutileping
type.magazine: Ajakiri
thread: Jutulõng
threads: Jutulõngad
microblog: Mikroblogi
people: Inimesed
events: Sündmused
magazine: Ajakiri
magazines: Ajakirjad
search: Otsi
add: Lisa
select_channel: Vali kanal
login: Logi sisse
sort_by: Järjestus
top: Parimad
hot: Hetkel teemaks
active: Aktiivne
newest: Uusim
oldest: Vanim
commented: Kommenteeritud
change_view: Muuda vaadet
filter_by_time: Filtreeri aja alusel
filter_by_type: Filtreeri tüübi alusel
filter_by_subscription: Filtreeri tellimuse alusel
filter_by_federation: Filtreeri födereerimise oleku alusel
comments_count: '{0} kommentaari|{1} kommentaar|]1,Inf[ kommentaari'
subscribers_count: '{0} tellijat|{1} tellija|]1,Inf[ tellijat'
followers_count: '{0} jälgijat|{1} jälgija|]1,Inf[ jälgijat'
marked_for_deletion: Märgitud kustutamiseks
marked_for_deletion_at: Märgitud kustutamiseks %date%
avatar: Tunnuspilt
added: Lisatud
up_votes: Hoolisamised
down_votes: Mahahääletused
no_comments: Kommentaare pole
created_at: Loodud
owner: Omanik
subscribers: Tellijad
online: Võrgus
comments: Kommentaarid
posts: Postitused
replies: Vastused
moderators: Moderaatorid
mod_log: Modereerimislogi
add_comment: Lisa kommentaar
add_post: Lisa postitus
add_media: Lisa meediat
remove_media: Eemalda meedia
remove_user_avatar: Eemalda tunnuspilt
remove_user_cover: Eemalda kaanepilt
local_and_federated: Kohalik ja födiversumis
filter.fields.only_names: Ainult nimed
filter.fields.names_and_descriptions: Nimed ja kirjedused
toolbar.bold: Paks kiri
toolbar.italic: Kaldkiri
toolbar.strikethrough: Läbikriipsutatud kiri
toolbar.header: Päis
toolbar.quote: Tsitaat
toolbar.code: Koodilõik
toolbar.link: Link
toolbar.image: Pilt
toolbar.unordered_list: Järjestamata loend
toolbar.ordered_list: Järjestatud loend
toolbar.mention: Mainimine
================================================
FILE: translations/messages.eu.yaml
================================================
thread: Haria
people: Jendea
search: Bilatu
type.link: Esteka
type.article: Haria
type.photo: Argazkia
type.video: Bideoa
type.magazine: Aldizkaria
threads: Hariak
magazine: Aldizkaria
magazines: Aldizkariak
add: Gehitu
login: Hasi saioa
oldest: Zaharrena
more: Gehiago
added: Gehituta
instances: Instantziak
newest: Berriena
favourites: Faboritoak
moderators: Moderatzaileak
owner: Jabe
password: Pasahitza
stats: Estatistikak
fediverse: Fedibertsoa
email: Helbide elektronikoa
reply: Erantzun
follow: Jarraitu
unfollow: Ez jarraitu
subscribe: Harpidetu
remember_me: Gogora nazazu
privacy_policy: Pribatutasun-politika
hidden: Ezkututa
help: Laguntza
try_again: Saiatu berriro
title: Izenburua
tag: Etiketa
tags: Etiketak
badges: Intsigniak
body: Gorputza
name: Izena
columns: Zutabeak
user: Erabiltzailea
description: Deskribapena
domain: Domeinua
followers: Jarraitzaileak
following: Jarraitzen
subscriptions: Harpidetzak
rules: Arauak
people_federated: Federatuta
overview: ikuspegi orokorra
articles: Hariak
links: Estekak
photos: Argazkiak
videos: Bideoak
share: Partekatu
reason: Arrazoia
edit: Editatu
notifications: Jakinarazpenak
profile: Profila
messages: Mezuak
save: Gorde
domains: Domeinuak
votes: Botoak
new_password: Pasahitz berria
dark: Iluna
light: Argia
font_size: Letra tamaina
yes: Bai
no: Ez
left: Ezkerra
right: Eskuina
filters: Iragazkiak
message: Mezua
approve: Onartu
month: Hilabetea
months: Hilabeteak
week: Astea
weeks: Asteak
pages: Orrialdeak
toolbar.bold: Lodia
toolbar.italic: Etzana
toolbar.header: Izenburua
toolbar.link: Esteka
errors.server404.title: 404 Ez aurkitua
errors.server403.title: 403 Debekatuta
errors.server429.title: 429 Eskaera gehiegi
custom_css: CSS egokituta
2fa.verify: Egiaztatu
alphabetically: Alfabetikoki
announcement: Iragarkia
keywords: Hitz gakoak
details: Xehetasunak
hide: Ezkutatu
show: Erakutsi
and: eta
last_updated: Azken eguneraketa
notification_title_new_reply: Erantzun berria
notification_title_new_thread: Hari berria
version: Bertsioa
show_active_users: Erakutsi erabiltzaile aktiboak
search_type_entry: Hariak
bookmark_list_edit: Editatu
bookmarks: Markatzaileak
answered: erantzuta
show_new_icons: Erakutsi ikono berriak
show_users_avatars_help: Erabiltzailearen abatarraren irudia erakutsi.
show_magazines_icons_help: Aldizkariaren ikonoa erakutsi.
type.smart_contract: Kontratu adimendun
delete_content: Ezabatu edukia
subject_reported_exists: Eduki honen berri eman da.
2fa.remove: Kendu 2FA
show_subscriptions: Erakutsi harpidetzak
change_my_avatar: Aldatu nire abatarra
edit_my_profile: Editatu nire profila
delete_magazine: Ezabatu aldizkaria
restore_magazine: Berrezarri aldizkaria
page_width: Orrialdearen zabalera
action: Ekintza
abandoned: Abandonatuta
spoiler: Spoilerra
report_subject: Gaia
notification_title_new_signup: Erabiltzaile berri bat erregistratuta
test_push_message: Kaixo mundua!
table_of_contents: Edukien taula
bookmark_lists: Markatzaile zerrendak
email_verification_pending: Zure helbide elektronikoa egiaztatu behar duzu saioa
hasi aurretik.
show_thumbnails_help: Miniaturen irudiak erakutsi.
commented: Komentatua
followers_count: '{0}Jarratzaileak|{1}Jarratzaile|]1,Inf[ Jarratzaileak'
avatar: Abatarra
favourite: Faborito
subscribers: Harpidedunak
replies: Erantzunak
unsubscribe: Harpidetza kendu
username: Erabiltzaile-izena
create_new_magazine: Aldizkari berria sortu
all_magazines: Aldizkari guztiak
reset_password: Pasahitza berridatzi
empty: Hutsik
show_more: Erakutsi gehiago
repeat_password: Pasahitza berridatzi
email_confirm_title: Baieztatu zure helbide elektronikoa.
email_confirm_header: Kaixo! Baieztatu zure helbide elektronikoa.
select_magazine: Aukeratu aldizkari bat
image: Irudia
people_local: Lokala
moderated: Moderatua
image_alt: Irudi alternatiboaren testua
oauth2.grant.user.bookmark.add: Gehitu laster-markak
oauth2.grant.user.bookmark.remove: Kendu laster-markak
oauth2.grant.user.bookmark_list.edit: Editatu zure laster-marken zerrendak
oauth2.grant.user.bookmark_list.read: Irakurri zure laster-marken zerrendak
oauth2.grant.user.profile.edit: Editatu zure profila.
oauth2.grant.user.profile.read: Irakurri zure profila.
oauth2.grant.user.bookmark_list.delete: Ezabatu zure laster-marken zerrendak
oauth2.grant.user.profile.all: Irakurri eta editatu zure profila.
oauth2.grant.user.message.read: Irakurri zure mezuak.
2fa.code_invalid: Autentifikazio-kodeak ez du balio
two_factor_authentication: Autentifikazio-faktore bikoitza
two_factor_backup: Autentifikazio-faktore bikoitzako babeskopien kodeak
2fa.authentication_code.label: Autentifikazio-kodea
2fa.disable: Autentifikazio-faktore bikoitza desaktibatu
2fa.backup: Zure autentifikazio-faktore bikoitzako babeskopien kodeak
close: Itxi
page_width_fixed: Finkatua
user_badge_bot: Bot
sensitive_show: Klikatu erakusteko
sensitive_warning: Eduki sentikorra
sensitive_hide: Klikatu ezkutatzeko
reported_user: Jakinarazitako erabiltzailea
edited: editatua
related_entry: Erlazionatua
notification_title_new_post: Post berria
select_user: Aukeratu erabiltzaile bat
online: Linean
related_posts: Erlazionatutako postak
add_post: Gehitu posta
posts: Postak
add_new_link: Esteka berria gehitu
add_new_photo: Argazki berria gehitu
add_new_post: Post berria gehitu
add_new_video: Bideo berria gehitu
faq: FAQ
add_new_article: Hari berria gehitu
is_adult: 18+ / NSFW
related_tags: Erlazionatutako etiketak
tree_view: Arbola-bista
classic_view: Bista-klasikoa
compact_view: Bista-trinkoa
subscribed: Harpidetuta
logout: Saioa itxi
table_view: Taula-bista
moderate: Moderatu
are_you_sure: Ziur zaude?
menu: Menua
hide_adult: Ezkutatu NSFW edukia
filter_by_type: Iragazi motaren arabera
comments: Iruzkinak
random_posts: Ausazko postak
add_comment: Gehitu iruzkina
up_votes: Aldeko botoak
down_votes: Aurkako botoak
marked_for_deletion: Ezabatzeko markatuta
marked_for_deletion_at: '%date%an ezabatzeko markatuta'
flash_thread_edit_success: Haria arrakastaz editatu da.
related_entries: Hari erlazionatuak
delete_account: Ezabatu kontua
random_entries: Ausazko hariak
flash_thread_delete_success: Haria arrakastaz ezabatu da.
flash_mark_as_adult_success: Posta arrakastaz NSFW bezala markatu da.
new_password_repeat: Baieztatu pasahitz berria
change_email: Helbide elektronikoa aldatu
change_password: Pasahitza aldatu
read_all: Irakurri dena
show_all: Erakutsi dena
flash_thread_pin_success: Haria arrakastaz finkatu da.
too_many_requests: Muga gaindituta, mesedez, saiatu beranduago.
deleted: Egileak ezabatua
sidebar_position: Albo-barraren posizioa
status: Egoera
add_moderator: Gehitu moderatzailea
upload_file: Artxiboa igo
send_message: Bidali mezu zuzena
approved: Onartua
rejected: Errefusatua
pin: Finkatu
done: Egina
change_language: Hizkuntza aldatu
change: Aldatu
pinned: Finkatuta
users: Erabiltzaileak
article: Haria
year: Urtea
federated: Federatua
restore: Berrezarri
send: Bidali
sidebar: Albo-barra
dynamic_lists: Zerrenda dinamikoak
toolbar.strikethrough: Marratua
block: Blokeatu
unblock: Desblokeatu
oauth2.grant.moderate.magazine_admin.create: Aldizkari berriak sortu.
oauth2.grant.entry.create: Hari berriak sortu.
size: Tamaina
show_thumbnails: Miniaturak erakutsi
flash_magazine_edit_success: Aldizkaria arrakastaz editatu da.
ban: Debekatu
unban: Debekua kendu
ban_hashtag_btn: Hashtag debekatu
bans: Debekuak
icon: Ikono
preview: Aurrebista
contact_email: Harremanetarako helbide elektronikoa
admin_panel: Administrazio-panela
instance: Instantzia
type_search_term: Bilaketa-termino bat idatzi
active_users: Pertsona aktiboak
related_magazines: Aldizkari erlazionatuak
banned_instances: Instantzia debekatuak
kbin_intro_title: Esploratu Fedibertsoa
kbin_promo_title: Sortu zure instantzia
random_magazines: Ausazko aldizkariak
header_logo: Goiburuko logotipoa
filter.adult.hide: Ezkutatu NSFW
filter.adult.show: Erakutsi NSFW
filter.adult.only: NSFW bakarrik
local_and_federated: Bertakoa eta federatua
toolbar.image: Irudia
account_deletion_button: Ezabatu kontua
mod_log: Moderazio-erregistroa
contact: Kontaktua
email_verify: Baieztatu helbide elektronikoa
share_on_fediverse: Partekatu Fedibertsoan
edit_entry: Haria editatu
copy_url_to_fediverse: Kopiatu URL originala
edit_comment: Aldaketak gorde
delete: Ezabatu
edit_post: Posta editatu
appearance: Itxura
privacy: Pribatutasuna
new_email_repeat: Baieztatu posta elektroniko berria
error: Errorea
back: Atzera
server_software: Zerbitzariaren softwarea
search_type_all: Hariak + Mikroblogak
show_new_icons_help: Erakutsi ikonoa aldizkari/erabiltzaile berriarentzat (30
egun edo berriagoak)
settings: Doikuntzak
blocked: Blokeatuta
subscribers_count: '{0}Harpidedunak|{1}Harpidedun|]1,Inf[ Harpidedunak'
microblog: Mikrobloga
================================================
FILE: translations/messages.fi.yaml
================================================
people: Ihmiset
type.video: Video
thread: Ketju
microblog: Mikroblogi
threads: Ketjut
add: Lisää
search: Haku
commented: Kommentoitu
up_votes: Tehostukset
more: Lisää
added: Lisätty
comments: Kommentit
created_at: Luotu
mod_log: Moderointiloki
posts: Viestit
replies: Vastaukset
moderators: Moderaattorit
reply: Vastaa
login_or_email: Käyttäjätunnus tai sähköpostiosoite
password: Salasana
dont_have_account: Eikö sinulla ole tiliä?
you_cant_login: Unohditko salasanasi?
already_have_account: Onko sinulla jo tili?
register: Rekisteröidy
reset_password: Palauta salasana
username: Käyttäjätunnus
repeat_password: Toista salasana
about_instance: Tietoja
add_new_article: Lisää uusi ketju
add_new_link: Lisää uusi linkki
add_new_photo: Lisää uusi kuva
add_new_post: Lisää uusi viesti
add_new_video: Lisää uusi video
contact: Yhteydenotto
terms: Käyttöehdot
privacy_policy: Tietosuojakäytäntö
faq: UKK
rss: RSS
change_theme: Vaihda teema
check_email: Tarkista sähköpostisi
up_vote: Tehosta
email_confirm_expire: Huomioi, että linkki vanhenee tunnissa.
image: Kuva
image_alt: Kuvan vaihtoehtoinen teksti
name: Nimi
description: Kuvaus
rules: Säännöt
subscriptions: Tilaukset
user: Käyttäjä
people_local: Paikallinen
subscribed: Tilattu
people_federated: Federoitu
all: Kaikki
logout: Kirjaudu ulos
classic_view: Klassinen näkymä
compact_view: Kompakti näkymä
3h: 3 tuntia
6h: 6 tuntia
12h: 12 tuntia
1w: 1 viikko
1m: 1 kuukausi
1y: 1 vuosi
share: Jaa
edit: Muokkaa
are_you_sure: Oletko varma?
share_on_fediverse: Jaa fediversessä
edit_entry: Muokkaa ketjua
edit_comment: Tallenna muutokset
edit_post: Muokkaa viestiä
notifications: Ilmoitukset
blocked: Estetty
privacy: Yksityisyys
new_password_repeat: Vahvista uusi salasana
change_email: Vaihda sähköpostiosoite
change_password: Vaihda salasana
expand: Laajenna
theme: Teema
dark: Tumma
light: Vaalea
error: Virhe
default_theme: Oletusteema
default_theme_auto: Vaalea/tumma (havaitse automaattisesti)
solarized_auto: Solarized (havaitse automaattisesti)
boosts: Tehostukset
yes: Kyllä
no: Ei
rounded_edges: Pyöristetyt reunat
show_thumbnails: Näytä pikkukuvat
show_users_avatars: Näytä käyttäjien avatarit
read_all: Lue kaikki
infinite_scroll: Loputon vieritys
show_top_bar: Näytä yläpalkki
off: Pois
upload_file: Lähetä tiedosto
instances: Instanssit
federation: Federointi
filters: Suodattimet
icon: Kuvake
done: Valmis
pin: Kiinnitä
unpin: Poista kiinnitys
change_language: Vaihda kieli
pinned: Kiinnitetty
users: Käyttäjät
content: Sisältö
year: Vuosi
local: Paikallinen
instance: Instanssi
pages: Sivut
FAQ: UKK
admin_panel: Ylläpitäjän paneeli
registrations_enabled: Rekisteröinti käytössä
random_entries: Satunnaiset ketjut
active_users: Aktiiviset ihmiset
auto_preview: Median automaattinen esikatselu
banned_instances: Estetyt instanssit
captcha_enabled: Captcha käytössä
boost: Tehosta
return: Palaa
browsing_one_thread: Selaat vain yhtä ketjua keskustelussa! Kaikki kommentit ovat
näkyvillä viestisivulla.
infinite_scroll_help: Lataa lisää sisältöä automaattisesti, kun saavutat sivun alalaidan.
reload_to_apply: Lataa sivu uudelleen, jotta muutokset tulevat voimaan
auto_preview_help: Laajenna automaattisesti median esikatselut.
filter.fields.only_names: Vain nimet
filter.fields.names_and_descriptions: Nimet ja kuvaukset
kbin_bot: Mbin-agentti
toolbar.strikethrough: Yliviivaus
toolbar.code: Koodi
toolbar.unordered_list: Järjestämätön luettelo
toolbar.ordered_list: Järjestetty luettelo
account_deletion_title: Tilin poistaminen
account_deletion_button: Poista tili
custom_css: Mukautettu CSS
block: Estä
unblock: Poista esto
schedule_delete_account: Ajasta poistaminen
two_factor_backup: Kaksivaiheisen todennuksen varmistuskoodit
show_subscriptions: Näytä tilaukset
close: Sulje
flash_email_failed_to_sent: Sähköpostia ei voitu lähettää.
flash_user_edit_profile_success: Käyttäjäprofiilin asetukset tallennettu.
flash_user_edit_email_error: Sähköpostiosoitteen vaihtaminen epäonnistui.
flash_user_edit_password_error: Salasanan vaihtaminen epäonnistui.
edit_my_profile: Muokkaa omaa profiilia
all_time: Kaikelta ajalta
show: Näytä
back: Takaisin
last_updated: Viimeksi päivitetty
and: ja
notification_title_new_reply: Uusi vastaus
notification_title_new_thread: Uusi ketju
server_software: Palvelinohjelmisto
version: Versio
user_verify: Aktivoi tili
max_image_size: Tiedoston enimmäiskoko
type.link: Linkki
type.photo: Kuva
type.article: Ketju
select_channel: Valitse kanava
events: Tapahtumat
login: Kirjaudu sisään
favourites: Suosikit
subscribers: Tilaajat
hot: Kuumat
oldest: Vanhimmat
sort_by: Järjestä
top: Parhaimmat
active: Aktiivinen
newest: Uusimmat
no_comments: Ei kommentteja
favourite: Suosi
add_comment: Lisää kommentti
remove_media: Poista media
empty: Tyhjä
unfollow: Lopeta seuraaminen
owner: Omistaja
add_post: Lisää viesti
enter_your_post: Kirjoita viestisi
subscribe: Tilaa
follow: Seuraa
add_media: Lisää media
enter_your_comment: Kirjoita kommenttisi
unsubscribe: Lopeta tilaus
email: Sähköpostiosoite
remember_me: Muista minut
show_more: Näytä lisää
stats: Tilastot
photos: Kuvat
email_verify: Vahvista sähköpostiosoite
try_again: Yritä uudelleen
email_confirm_content: 'Olethan valmis aktivoidaksesi Mbin-tilisi? Napsauta linkkiä
alla:'
email_confirm_header: Hei! Vahvista sähköpostiosoitteesi.
email_confirm_title: Vahvista sähköpostiosoitteesi.
add_new: Lisää uusi
new_email: Uusi sähköpostiosoite
new_password: Uusi salasana
collapse: Supista
links: Linkit
articles: Ketjut
Your account is not active: Tilisi ei ole aktiivinen.
videos: Videot
messages: Viestit
save: Tallenna
homepage: Kotisivu
hide_adult: Piilota NSFW-sisältö
current_password: Nykyinen salasana
delete: Poista
profile: Profiili
old_email: Nykyinen sähköpostiosoite
new_email_repeat: Vahvista uusi sähköpostiosoite
font_size: Fontin koko
menu: Valikko
settings: Asetukset
appearance: Ulkoasu
general: Yleinen
size: Koko
show_all: Näytä kaikki
flash_register_success: Tervetuloa mukaan! Tilisi on nyt rekisteröity. Vielä yksi
asia - tarkista sähköpostisi ja napsauta vastaanottamaasi aktivointilinkkiä saattaaksesi
tilisi eloon.
created: Luotu
expired_at: Vanheni
firstname: Etunimi
sidebar_position: Sivupalkin sijainti
left: Vasen
right: Oikea
on: Päällä
status: Tila
expires: Vanhenee
perm: Pysyvä
delete_account: Poista tili
type_search_term: Kirjoita hakuehto
federation_enabled: Federaatio käytössä
Password is invalid: Salasana on virheellinen.
send: Lähetä
sidebar: Sivupalkki
kbin_intro_title: Selaa fediverseä
kbin_promo_title: Luo oma instanssi
mercure_enabled: Mercure käytössä
report_issue: Ilmoita ongelmasta
filter.adult.show: Näytä NSFW
kbin_promo_desc: '%link_start%Kloonaa tietovarasto%link_end% ja kehitä fediverseä'
toolbar.image: Kuva
filter.adult.hide: Piilota NSFW
filter.adult.only: Vain NSFW
toolbar.link: Linkki
account_deletion_immediate: Poista välittömästi
local_and_federated: Paikallinen ja federoitu
agree_terms: Hyväksy %terms_link_start%käyttöehdot%terms_link_end% ja %policy_link_start%tietosuojakäytäntö%policy_link_end%
reason: Syy
preview: Esikatselu
article: Ketju
reputation: Maine
toolbar.bold: Lihavointi
toolbar.italic: Kursivointi
toolbar.quote: Lainaus
oauth.consent.allow: Salli
oauth.consent.deny: Estä
flash_post_pin_success: Viesti on kiinnitetty.
two_factor_authentication: Kaksivaiheinen todennus
2fa.authentication_code.label: Todennuskoodi
2fa.code_invalid: Todennuskoodi ei ole kelvollinen
2fa.verify: Vahvista
cancel: Peruuta
password_and_2fa: Salasana ja 2FA
flash_email_was_sent: Sähköposti on lähetetty.
page_width: Sivun leveys
sensitive_show: Napsauta näyttääksesi
flash_user_settings_general_success: Käyttäjäasetukset tallennettu.
sensitive_hide: Napsauta piilottaaksesi
hide: Piilota
cake_day: Kakkupäivä
show_active_users: Näytä aktiiviset käyttäjät
notification_title_new_comment: Uusi kommentti
random_posts: Satunnaiset viestit
1d: 1 päivä
page_width_fixed: Kiinteä
kbin_intro_desc: on hajautettu alusta sisällön yhteenkokoamiseen sekä mikrobloggaukseen,
ja se toimii osana Fediverse-verkkoa.
show_profile_subscriptions: Näytä makasiinitilaukset
show_related_entries: Näytä satunnaisia ketjuja
show_related_posts: Näytä satunnaisia viestejä
filter_by_federation: Suodata federoinnin tilan mukaan
filter_by_time: Suodata ajan mukaan
subscribers_count: '{0}tilaajaa|{1}tilaaja|]1,Inf[ tilaajaa'
marked_for_deletion: Merkitty poistettavaksi
marked_for_deletion_at: Merkitty poistettavaksi %date%
subscriptions_in_own_sidebar: Erillisessä sivupalkissa
sidebars_same_side: Sivupalkit samalla puolella
type.magazine: Makasiini
magazine: Makasiini
magazines: Makasiinit
change_view: Vaihda näkymää
comments_count: '{0}kommenttia|{1}kommentti|]1,Inf[ kommenttia'
followers_count: '{0}seuraajaa|{1}seuraaja|]1,Inf[ seuraajaa'
filter_by_type: Suodata tyypin mukaan
filter_by_subscription: Suodata tilauksen mukaan
markdown_howto: Kuinka muokkain toimii?
related_posts: Asiaan liittyvät viestit
always_disconnected_magazine_info: Tämä makasiini ei vastaanota päivityksiä.
subscribe_for_updates: Tilaa vastaanottaaksesi päivityksiä.
all_magazines: Kaikki makasiinit
create_new_magazine: Luo uusi makasiini
useful: Hyödyllinen
down_vote: Vähennä
select_magazine: Valitse makasiini
followers: Seuraajat
go_to_content: Siirry sisältöön
go_to_filters: Siirry suodattimiin
go_to_search: Siirry hakuun
tree_view: Puunäkymä
chat_view: Keskustelunäkymä
table_view: Taulukkonäkymä
cards_view: Korttinäkymä
copy_url: Kopioi Mbin-osoite
copy_url_to_fediverse: Kopioi alkuperäinen osoite
show_magazines_icons: Näytä makasiinien kuvakkeet
subject_reported: Sisällöstä on ilmoitettu.
change_magazine: Vaihda makasiinia
related_magazines: Aiheeseen liittyvät makasiinit
random_magazines: Satunnaiset makasiinit
comment_reply_position: Kommentin vastauksen sijainti
subscription_header: TIlatut makasiinit
position_bottom: Alhaalla
position_top: Ylhäällä
flash_account_settings_changed: Tilisi asetukset on muutettu onnistuneesti. Sinun
täytyy kirjautua uudelleen sisään.
subscription_sort: Järjestä
alphabetically: Aakkosjärjestys
subscription_sidebar_pop_out_right: Siirrä erilliseen sivupalkkiin oikealle
subscription_sidebar_pop_out_left: Siirrä erilliseen sivupalkkiin vasemmalle
flash_user_edit_profile_error: Profiilin asetusten tallentaminen epäonnistui.
flash_user_settings_general_error: Käyttäjän asetusten tallentaminen epäonnistui.
magazine_deletion: Makasiinin poisto
delete_magazine: Poista makasiini
restore_magazine: Palauta makasiini
open_url_to_fediverse: Avaa alkuperäinen osoite
magazine_is_deleted: Makasiini on poistettu. Voit palauttaa
sen 30 päivän sisällä.
request_magazine_ownership: Pyydä makasiinin omistajuutta
accept: Hyväksy
abandoned: Hylätty
cancel_request: Peru pyyntö
sso_registrations_enabled: Kertakirjautumisrekisteröinnit käytössä
edited: muokattu
keywords: Avainsanat
notification_title_mention: Sinut mainittiin
notification_title_removed_comment: Kommentti poistettiin
notification_title_edited_comment: Kommenttia muokattiin
notification_title_removed_thread: Ketju poistettiin
notification_title_edited_thread: Ketjua muokattiin
show_related_magazines: Näytä satunnaisia makasiineja
new_user_description: Tämä käyttäjä on uusi (aktiivinen alle %days% päivää)
new_magazine_description: Tämä makasiini on uusi (aktiivinen alle %days% päivää)
hidden: Piilotettu
enabled: Käytössä
title: Otsikko
is_adult: 18+ / NSFW
overview: Yleisnäkymä
columns: Sarakkeet
joined: Liittynyt
rejected: Hylätty
help: Tuki
url: URL-osoite
body: Sisältö
tags: Tunnisteet
tag: Tunniste
cards: Kortit
featured_magazines: Esittelyssä olevat makasiinit
votes: Äänet
reject: Hylkää
approve: Hyväksy
approved: Hyväksytty
dashboard: Kojelauta
registration_disabled: Rekisteröinti poistettu käytöstä
password_confirm_header: Vahvista salasanan vaihtopyyntö.
notification_title_new_report: Uusi raportti luotiin
comment_not_found: Kommenttia ei löydy
table_of_contents: Sisällysluettelo
================================================
FILE: translations/messages.fil.yaml
================================================
add: Magdagdag
people: Mga tao
oldest: Pinakaluma
owner: May-ari
replies: Mga tugon
sort_by: Isaayos ayon sa
more: Higit pa
favourites: Mga paborito
newest: Pinakabago
search: Maghanap
added: Idinagdag
show_more: Ipakita ang higit pa
in: sa
rules: Mga patakaran
followers: Mga tagasunod
following: Sinusundan
go_to_content: Pumunta sa nilalaman
all: Lahat
3h: 3o
report: Iulat
reason: Dahilan
reports: Mga pagulat
dark: Madilim
light: Maliwanag
title: Pamagat
try_again: Subukang muli
from: mula
about_instance: Tungkol dito
help: Tulong
add_new: Magdagdag ng bago
6h: 6o
1m: 1b
1y: 1t
12h: 12o
1w: 1l
1d: 1a
save: IImbak
subscription_sort: Isaayos
up_votes: Mga pagpalakas
down_votes: Mga pagpahina
created_at: Ginawa
comments: Mga Puna
add_comment: Magdagdag ng puna
enter_your_comment: Ipasok ang iyong puna
login: Mag-log in
active: Aktibo
no_comments: Walang mga puna
moderators: Mga tagatimpi
mod_log: Talaan ng pagtitimpi
add_post: Magdagdag ng post
activity: Aktibidad
cover: Pangtakip
empty: Walang laman
follow: Sundan
unfollow: Huwag sundan
reply: Tumugon
register: Mag-rehistro
to: sa
up_vote: Palakasin
down_vote: Pahinain
copy_url: Kopyahin ang URL sa Mbin
copy_url_to_fediverse: Kopyahin ang orihinal na URL
delete: Burahin
about: Tungkol dito
off: Nakapatay
reject: Tanggihan
done: Tapos na
change: Baguhin
preview: Paunang tingin
content: Nilalaman
week: Linggo
weeks: (na) linggo
month: Buwan
months: (na) buwan
year: Taon
send: Ipadala
delete_account: Tanggalin ang account
purge_account: Purgahin ang account
return: Bumalik
followers_count: '{0}Mga tagasunod|{1}Tagasunod|]1,Inf[Mga tagasunod'
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
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.
online: Nasa linya
markdown_howto: Paano gumagana ang editor?
random_posts: Pasadyang mga post
related_posts: Kaugnay na mga post
remember_me: Tandaan ako
terms: Mga tuntunin ng serbisyo
stats: Istatistika
body: Katawan
name: Pamagat
description: Paglalarawan
go_to_search: Pumunta sa paghahanap
messages: Mga mensahe
featured_magazines: Itinatampok na mga magasin
notify_on_new_post_reply: Mga tugon ng anumang antas sa mga post na inaakda ko
notify_on_new_post_comment_reply: Mga tugon sa aking puna sa anumang mga post
theme: Tema
boosts: Mga pagpalakas
read_all: Basahin lahat
flash_magazine_edit_success: Matagumpay na nabago ang magasin na ito.
banned: Binawalan ka
send_message: Magpadala ng direktang mensahe
message: Mensahe
status: Katayuan
change_language: Baguhin ang wika
change_magazine: baguhin ang magasin
mark_as_adult: Markahin bilang NSFW
registrations_enabled: Pinagana ang pagrehistro
registration_disabled: Nakapatay ang pagrehistro
report_issue: Iulat ang isyu
kbin_bot: Ahente sa Mbin
account_deletion_title: Pagbura ng account
account_deletion_button: Burahin ang account
more_from_domain: Higit pa mula sa domain
single_settings: Pang-isahan
continue_with: Magpatuloy gamit ang
purge: Purgahin
show_all: Ipakita lahat
deleted: Tinanggal ng may-akda
mentioned_you: Binabanggit sa iyo
filter_by_type: Salain ayon sa uri
alphabetically: Paalpabetiko
cancel: Kanselahin
position_top: Itaas
pending: Nakabinbin
close: Isara
direct_message: Direktang mensahe
top: Nangunguna
edited_post: Binago ang post
edited_comment: Binago ang puna
last_active: Huling Aktibo
comment_reply_position: Posisyon ng tugon sa puna
position_bottom: Ibaba
edited: binago
and: at
back: Bumalik
mod_remove_your_post: Binura ang iyong post ng isang tagatimpi
moderation.report.reject_report_confirmation: Sigurado ka bang tanggihan ang
ulat na ito?
open_url_to_fediverse: Buksan ang orihinal na URL
deletion: Pagbura
delete_magazine: Burahin ang magasin
details: Mga detalye
deleted_by_author: Binura ng may-akda ang [thread], [post] o puna
report_accepted: Natanggap ang isang ulat
own_content_reported_accepted: Natanggap ang isang ulat ng iyong nilalaman.
own_report_accepted: Natanggap ang iyong ulat
own_report_rejected: Naitanggi ang iyong ulat
reported_user: Iniulat na tagagamit
reporting_user: Paguulat sa tagagamit
last_successful_receive: Huling matagumpay na pagtanggap
notification_title_message: Bagong direktang mensahe
hide: Itago
show: Ipakita
purge_magazine: Purgahin ang magasin
sensitive_hide: Pindutin upang itago
sensitive_show: Pindutin upang ipakita
sensitive_warning: Sensitibong nilalaman
notification_title_mention: Binabanggit ka
notification_title_edited_comment: Nabago ang puna
notification_title_new_reply: Bagong tugon
notification_title_removed_comment: Natanggal ang puna
notification_title_ban: Binabawalan ka
max_image_size: Pinakamataas na laki ng file
hidden: Nakatago
enabled: Pinagana
notification_title_new_report: Nagawa ang isang bagong ulat
notification_title_edited_post: Nabago ang isang post
notification_title_removed_post: Natanggal ang isang post
magazine_posting_restricted_to_mods_warning: Mga tagatimpi lamang ang gumagawa
ng mga thread sa magasin na ito
open_report: nakabukas na ulat
already_have_account: Mayroon ka na bang account?
eng: ENG
comment_not_found: Hindi nakita ang puna
subscription_header: Mga naka-subscribe na mga magasin
flash_user_settings_general_success: Matagumpay na nakaimbak ang mga setting ng
tagagamit.
subscribed: Naka-subscribe
old_email: Kasalukuyang email
restored_comment_by: binalik ang puna ni
show_related_magazines: Ipakita ang mga pasadyang magasin
hot: Patok
reset_check_email_desc2: Kapag hindi ka nakatanggap ng "e-mail" mangyaring
tingnan ang folder ng spam.
settings: Mga setting
hide_adult: Itago ang nilalaman na NSFW
notify_on_new_posts: Bagong mga post sa anumang magasin sa saan ako
naka-subscribe
writing: Pagsusulat
firstname: Unang pangalan
active_users: Mga aktibong tao
boost: Palakasin
all_time: Lahat ng oras
random_magazines: Mga pasadyang magasin
restore: Ibalik
flash_user_settings_general_error: Nabigong iimbak ang mga setting ng tagagamit.
disconnected_magazine_info: Hindi tumatangap ng mga update ang magasin na ito
(huling aktibidad noong %days% (na) araw ang nakalipas).
always_disconnected_magazine_info: Hindi tumatangap ng mga update ang magasin na
ito.
edit_comment: Iimbak ang nga pagbabago
notify_on_new_entry_reply: Mga puna sa anumang antas sa mga thread na
pinag-akdaan ko
collapse: Paliitin
removed_thread_by: tinanggal ang thread ni
restored_thread_by: ibinalik ang thread ni
removed_comment_by: tinanggal ang puna ni
removed_post_by: tinanggal ang post ni
restored_post_by: ibinalik ang post ni
he_banned: binawalan
flash_register_success: Maligayang paglalakbay! Nakarehistro na ang iyong
account. Isang huling hakbang - tingnan ang iyong inbox para sa isang link ng
pag-activate na magbibigay-buhay sa iyong account.
mod_deleted_your_comment: Tinanggal ng isang tagatimpi ang iyong puna
added_new_post: Idinagdag ang bagong post
added_new_reply: Idinagdag ang bagong tugon
wrote_message: Isinulat ang isang mensahe
removed: Tinanggal ng tagatimpi
comment: Puna
replied_to_your_comment: Tinutugon sa iyong puna
added_new_comment: Idinagdag ang isang bagong puna
mod_remove_your_thread: Tinanggal ng isang tagatimpi ang iyong thread
edited_thread: Binago ang thread
added_new_thread: Idinagdag ang isang bagong puna
mod_log_alert: BABALA - Maaaring naglalaman ang talaang ito ng hindi kasiya-siya
o nakakabagabag na nilalaman na tinanggal ng mga tagatimpi. Mangyaring
mag-ingat.
kbin_promo_title: Gumawa ng iyong sariling instance
filter.origin.label: Piliin ang pinagmulan
filter_labels: Salain ang mga label
auto_preview: Automatikong paunang tigin ng media
bookmark_add_to_list: Idagdag ang bookmark sa %list%
bookmark_add_to_default_list: Idagdag ang bookmark sa pangunahing talaan
magazine_posting_restricted_to_mods: Limitahin ang paggawa ng thread sa mga
tagatimpi
notification_title_new_signup: Nakarehisto ang isang bagong tagagamit
notification_body_new_signup: Nagrehisto ang tagagamit na si %u%.
notification_body2_new_signup_approval: Kailangan mong tanggapin ang hiling bago
sila maka-log-in
notification_title_edited_thread: Nabago ang isang thread
last_updated: Huling nabago
someone: May
edit_post: Baguhin ang post
oauth2.grant.post.report: Iulat ang anumang post.
oauth2.grant.post.create: Gumawa ng mga bagong post.
================================================
FILE: translations/messages.fr.yaml
================================================
type.article: Fil de discussion
type.photo: Photo
type.video: Vidéo
thread: Fil
threads: Fils
microblog: Microblogue
events: Événements
magazine: Magazine
magazines: Magazines
search: Recherche
add: Ajouter
people: Personnes
login: Se connecter
top: Top
active: Actif
favourites: Favoris
favourite: Favori
more: Plus
type.magazine: Magazine
newest: Plus récents
oldest: Plus anciens
commented: Commentés
filter_by_time: Filtrer par période
filter_by_type: Filtrer par type
comments_count: '{0}Commentaire|{1}Commentaire|]1,Inf[ Commentaires'
added: Ajouté
no_comments: Aucun commentaire
created_at: Créé
owner: Propriétaire
subscribers: Abonné·e·s
online: En ligne
posts: Publications
replies: Réponses
moderators: Modérateur·rice·s
mod_log: Journal de modération
add_comment: Ajouter un commentaire
add_post: Ajouter une publication
markdown_howto: Comment fonctionne l’éditeur ?
enter_your_comment: Entrez votre commentaire
enter_your_post: Entrez votre message
activity: Activité
related_posts: Publications liées
type.link: Lien
select_channel: Sélectionnez une chaîne
avatar: Avatar
comments: Commentaires
random_posts: Publications aléatoires
change_view: Changer la vue
add_media: Ajouter média
up_votes: Partages
down_votes: Réductions
cover: Couverture
federated_magazine_info: Les magazines provenant d’un serveur fédéré peuvent
être incomplets.
federated_user_info: Les profils provenant d’un serveur fédéré peuvent être
incomplets.
go_to_original_instance: Parcourez-en d’avantage sur l’instance d’origine.
empty: Vide
subscribe: S’abonner
unsubscribe: Se désabonner
follow: Suivre
unfollow: Ne plus suivre
reply: Répondre
login_or_email: Nom d’utilisateur ou adresse e-mail
password: Mot de passe
remember_me: Rester connecté·e
dont_have_account: Vous n’avez pas de compte ?
you_cant_login: Mot de passe oublié ?
already_have_account: Vous avez déjà un compte ?
register: S’inscrire
reset_password: Réinitialiser le mot de passe
to: vers
username: Nom d’utilisateur
email: E-mail
repeat_password: Répéter le mot de passe
terms: Conditions d’utilisation du service
about_instance: À propos
all_magazines: Tous les magazines
stats: Statistiques
fediverse: Fédivers
create_new_magazine: Créer un nouveau magazine
add_new_link: Ajouter un nouveau lien
add_new_photo: Ajouter une nouvelle photo
add_new_post: Ajouter une nouvelle publication
add_new_video: Ajouter une nouvelle vidéo
contact: Contact
faq: FAQ
rss: RSS
change_theme: Changer le thème
useful: Utile
help: Aide
try_again: Veuillez réessayer
up_vote: Partager
down_vote: Réduire
email_verify: Confirmer l’adresse e-mail
email_confirm_expire: Veuillez noter que le lien expirera dans une heure.
email_confirm_title: Confirmez votre adresse e-mail.
select_magazine: Sélectionnez un magazine
add_new: Ajouter nouveau
url: URL
title: Titre
body: Corps
tags: Étiquettes
badges: Insignes
is_adult: +18 / NSFW
email_confirm_content: 'Prêt pour activer votre compte Mbin ? Cliquez sur le lien
ci-dessous :'
oc: CO
image_alt: Texte alternatif de l’image
name: Nom
description: Description
rules: Règles
domain: Domaine
followers: Suiveur·se·s
following: Suivis
overview: Vue d'ensemble
cards: Cartes
columns: Colonnes
user: Utilisateur
moderated: Modéré·e
people_local: Local
people_federated: Fédéré
reputation_points: Points de réputation
related_tags: Étiquettes liées
all: Tous
logout: Se déconnecter
compact_view: Vue compacte
3h: 3 h
6h: 6 h
12h: 12 h
1d: 1 j
1w: 1 sem
1m: 1 m
1y: 1 an
links: Liens
articles: Fils
photos: Photos
report: Signaler
share: Partager
copy_url: Copier l'URL de Mbin
go_to_content: Aller au contenu
go_to_filters: Aller aux filtres
classic_view: Vue classique
chat_view: Vue de discussion
tree_view: Vue arborescente
table_view: Vue en table
cards_view: Vue en cartes
moderate: Modérer
reason: Motif
copy_url_to_fediverse: Copier l'URL d'origine
share_on_fediverse: Partager sur le Fédivers
delete: Supprimer
edit_post: Modifier le message
edit_comment: Enregistrer les modifications
settings: Réglages
general: Général
profile: Profil
blocked: Bloqué·e
reports: Signalements
notifications: Notifications
appearance: Apparence
homepage: Page d’accueil
hide_adult: Masquer le contenu réservé aux adultes
featured_magazines: Magazines en vedette
privacy: Confidentialité
show_more: Afficher plus
add_new_article: Ajouter un nouveau fil
check_email: Vérifiez vos e-mails
eng: ENG
subscribed: Abonné·e
videos: Vidéos
are_you_sure: Êtes-vous sûr·e ?
reset_check_email_desc2: Si vous ne recevez pas d'e-mail, veuillez vérifier
votre dossier spam.
messages: Messages
in: dans
email_confirm_header: Salut ! Confirmez votre adresse e-mail.
image: Image
edit: Modifier
go_to_search: Aller à la recherche
privacy_policy: Politique de confidentialité
subscriptions: Abonnements
type.smart_contract: Contrat intelligent
hot: Dernière minute
agree_terms: Consentir aux %terms_link_start%conditions
d’utilisation%terms_link_end% et à la %policy_link_start%politique de
confidentialité%policy_link_end%
reset_check_email_desc: Si un compte est déjà associé à votre adresse e-mail,
vous devriez recevoir sous peu un message contenant un lien que vous pourrez
utiliser pour réinitialiser votre mot de passe. Ce lien expirera en %expire%.
joined: Inscrit·e
show_profile_subscriptions: Afficher les magazines souscrits
show_profile_followings: Afficher les utilisateurs abonnés
notify_on_new_entry_reply: Tous les commentaires dans les fils de discussion que
j'ai créés
notify_on_new_entry_comment_reply: Réponses à mes commentaires dans tous les
fils de discussion
notify_on_new_post_reply: Toutes les réponses aux messages que j'ai créés
notify_on_new_post_comment_reply: Réponses à mes commentaires sur les messages
notify_on_new_entry: Nouveaux fils de discussion (liens ou articles) dans
n'importe quel magazine auquel je suis abonné
notify_on_new_posts: Nouveaux articles dans n'importe quel magazine auquel je
suis abonné
save: Enregistrer
about: À propos
old_email: E-mail actuel
new_email: Nouvelle adresse e-mail
new_email_repeat: Confirmer la nouvelle adresse e-mail
current_password: Mot de passe actuel
new_password: Nouveau mot de passe
new_password_repeat: Confirmer le nouveau mot de passe
change_email: Changer d'adresse e-mail
change_password: Changer le mot de passe
expand: Élargir
collapse: Réduire
domains: Domaines
error: Erreur
votes: Votes
theme: Thème
dark: Sombre
light: Clair
font_size: Taille de police
size: Taille
he_unbanned: débloquer
boosts: Partages
yes: Oui
no: Non
show_thumbnails: Afficher les vignettes
show_users_avatars: Afficher les avatars des utilisateurs
ban_expired: L'interdiction a expiré
ban: Bannir
firstname: Prénom
bans: Interdictions
add_ban: Ajouter une interdiction
return: Retour
header_logo: Logo d'entête
captcha_enabled: Captcha activé
kbin_promo_title: Créer votre propre instance
kbin_intro_title: Explorer le Fédivers
dynamic_lists: Listes dynamiques
auto_preview: Aperçu automatique des médias
sidebar: Barre latérale
random_magazines: Magazines aléatoires
related_magazines: Magazines reliés
unban_account: Débloquer le compte
delete_account: Supprimer le compte
related_entries: Fils reliés
random_entries: Fils aléatoires
active_users: Personnes actives
banned_instances: Instances bannies
ban_account: Bannir le compte
send: Envoyer
he_banned: bannir
Your account has been banned: Votre compte a été banni.
banned: Vous a banni
show_magazines_icons: Afficher les icônes des magazines
solarized_light: Clair solarisé
solarized_dark: Sombre solarisé
rounded_edges: Bords arrondis
removed_thread_by: a supprimé le fil de
restored_thread_by: a rétabli le fil de
removed_comment_by: a supprimé le commentaire de
restored_comment_by: a rétabli le commentaire de
removed_post_by: a supprimé une publication de
restored_post_by: a rétabli la publication de
read_all: Lire tout
show_all: Afficher tout
flash_register_success: Bienvenue à bord ! Votre compte est maintenant
enregistré. Une dernière étape - vérifiez votre boîte de réception pour y
trouver un lien d'activation qui donnera vie à votre compte.
flash_thread_new_success: Le fil a été créé et est maintenant visible par les
autres utilisateurs.
flash_thread_edit_success: Le fil a été modifié.
flash_thread_delete_success: Le fil a été supprimé.
flash_thread_pin_success: Le fil a été épinglé.
flash_thread_unpin_success: Le fil a été désépinglé.
flash_magazine_edit_success: Le magazine a été modifié.
too_many_requests: Limite dépassée, veuillez réessayer plus tard.
set_magazines_bar: Barre des magazines
set_magazines_bar_desc: ajoutez les noms de magazines après la virgule
edited_thread: Fil modifié
added_new_thread: a ajouté un nouveau fil
added_new_comment: A ajouté un nouveau commentaire
edited_comment: A modifié un commentaire
replied_to_your_comment: A répondu à un de vos commentaires
mod_deleted_your_comment: Un(e) modérateur/trice a supprimé votre commentaire
mod_remove_your_thread: Un(e) modérateur/trice a retiré votre fil
added_new_post: A ajouté un nouveau message
edited_post: A modifié un message
mod_remove_your_post: Un(e) modérateur/trice a retiré votre message
added_new_reply: A ajouté une nouvelle réponse
removed: Retiré par un(e) modérateur/trice
deleted: Supprimé par l'auteur
mentioned_you: vous a mentionné
comment: Commentaire
post: Message
purge: Purger
message: Message direct
infinite_scroll: Défilement infini
show_top_bar: Afficher la barre supérieure
sticky_navbar: Barre de navigation collante
subject_reported: Le contenu a été signalé.
left: Gauche
right: Droite
federation: Fédération
status: Statut
on: On
off: Inactif
instances: Instances
upload_file: Envoyer un fichier
from_url: Depuis l'URL
magazine_panel: Panneau de magazine
reject: Refuser
approve: Approuver
filters: Filtres
approved: Approuvé
rejected: Rejeté
add_moderator: Ajouter un(e) modérateur/trice
add_badge: Ajouter un badge
created: Créé
expires: Expire
perm: Permanent
expired_at: Expiré le
trash: Corbeille
icon: Icône
done: Terminé
pin: Épingler
flash_magazine_new_success: Le magazine a été créé. Vous pouvez désormais
ajouter du nouveau contenu ou explorer le panneau d’administration du
magazine.
set_magazines_bar_empty_desc: si le champ est vide, les magazines actifs seront
affichés sur la barre.
wrote_message: A écrit un message
send_message: Envoyer un message
sidebar_position: Position de la barre latérale
mod_log_alert: ATTENTION - Le journal de modération peut contenir des éléments
désagréables ou choquants qui ont été supprimés par les modérateurs. Soyez
prudents.
unpin: Détacher
change_magazine: Changer de magazine
change_language: Changer de langue
change: Changer
pinned: Épinglé
preview: Aperçu
article: Fil
reputation: Réputation
note: Note
writing: Écriture
users: Utilisateurs/trices
content: Contenu
week: Semaine
weeks: Semaines
month: Mois
months: Mois
year: Année
federated: Fédéré
local: Local
admin_panel: Panneau d'administration
dashboard: Tableau de bord
contact_email: Email de contact
meta: Méta
instance: Instance
pages: Pages
FAQ: FAQ
type_search_term: Tapez le terme de recherche
federation_enabled: Fédération active
registrations_enabled: Inscriptions ouvertes
registration_disabled: Inscriptions fermées
restore: Restaurer
add_mentions_entries: Ajouter des étiquettes dans le contenu
add_mentions_posts: Ajouter des étiquettes dans les messages
Password is invalid: Le mot de passe n'est pas valide.
Your account is not active: Votre compte n'est pas actif.
purge_account: Purger le compte
magazine_panel_tags_info: Fournissez seulement si vous voulez inclure du contenu
du Fédivers dans ce magazine, d'après ces étiquettes
kbin_intro_desc: est une plate-forme décentralisée d'agrégation de contenu et de
microblogging qui fonctionne au sein du réseau Fédivers.
kbin_promo_desc: '%link_start%Clonez le dépôt%link_end% et développez le Fédivers'
browsing_one_thread: Vous ne parcourez qu'un seul fil de discussion ! Tous les
commentaires sont disponibles sur la page de publication.
boost: Partager
mercure_enabled: Mercure activé
report_issue: Signaler un problème
tokyo_night: Nuit tokyoïte
oauth2.grant.post.edit: Editer de nouvelles publications.
oauth2.grant.moderate.post.trash: Supprimer ou restaurer des publications dans
vos magazines modérés.
moderation.report.approve_report_title: Approuver le rapport
moderation.report.reject_report_title: Rejeter le rapport
kbin_bot: Robot Mbin
oauth2.grant.moderate.magazine.reports.all: Gérer les rapports dans vos
magazines modérés.
oauth2.grant.admin.federation.update: Ajouter ou supprimer des instances dans ou
à partir de la liste des instances défédérées.
filter.adult.label: Choisir d'afficher ou non du contenu NSFW
resend_account_activation_email_error: Un problème est survenu lors de la
présentation de cette demande. Il se peut qu'aucun compte ne soit associé à
cet e-mail ou qu'il soit déjà activé.
oauth2.grant.user.message.all: Afficher vos messages et envoyer des messages à
d'autres utilisateurs.
oauth2.grant.moderate.magazine.trash.read: Afficher le contenu mis à la
corbeille dans vos magazines modérés.
email_confirm_button_text: Confirmez votre demande de modification de mot de
passe
oauth2.grant.moderate.magazine_admin.create: Créer de nouveaux magazines.
filter.adult.hide: Masquer le contenu NSFW
oauth2.grant.post.vote: Upvoter, booster, ou downvoter n'importe quelle
publication.
magazine_theme_appearance_custom_css: CSS personnalisé qui s'appliquera lors de
l'affichage du contenu dans votre magazine.
toolbar.bold: Gras
errors.server429.title: 429 Trop de requêtes
auto_preview_help: Afficher automatiquement les aperçus multimédias.
filter.fields.label: Choisissez les champs à rechercher
toolbar.header: En-tête
oauth2.grant.user.oauth_clients.edit: Modifier les autorisations que vous avez
accordées à d'autres applications OAuth2.
oauth2.grant.user.all: Afficher et modifier votre profil, vos messages ou vos
notifications ; Afficher et modifier les autorisations que vous avez accordées
à d'autres applications ; suivre ou bloquer d'autres utilisateurs ; Afficher
les listes d'utilisateurs que vous suivez ou bloquez.
oauth2.grant.moderate.post.set_adult: Marquer les publications comme NSFW dans
vos magazines modérés.
oauth2.grant.moderate.magazine_admin.edit_theme: Modifier le CSS personnalisé de
l'un de vos magazines propriétaires.
oauth2.grant.moderate.magazine_admin.tags: Créer ou supprimer les tags des
magazines qui vous appartiennent.
moderation.report.ban_user_description: Voulez-vous bannir l'utilisateur
(%username%) qui a créé ce contenu à partir de ce magazine ?
oauth2.grant.moderate.entry.pin: Épingler des fils en haut de vos magazines
modérés.
oauth2.grant.user.message.read: Afficher vos messages.
oauth2.grant.admin.entry.purge: Supprimer complètement tout les fils de
discussion de votre instance.
oauth.consent.to_allow_access: Pour autoriser cet accès, cliquez sur le bouton «
Autoriser » ci-dessous
oauth2.grant.admin.magazine.all: Déplacer les fils de discussions entre les
magazines de votre instance ou les supprimer complètement.
filter.fields.only_names: Les noms seulement
oauth2.grant.admin.instance.settings.read: Afficher les paramètres de votre
instance.
oauth2.grant.entry.report: Signaler n'importe quel thread.
oauth2.grant.moderate.post_comment.all: Modérer les commentaires sur les
publications dans vos magazines modérés.
local_and_federated: Local et fédéré
email.delete.description: L'utilisateur suivant a demandé que son compte soit
supprimé
last_active: Dernière activité
oauth2.grant.domain.subscribe: S'abonner ou se désabonner des domaines et
afficher les domaines auxquels vous êtes abonné.
magazine_theme_appearance_icon: Icône personnalisée pour le magazine. Si aucune
n'est sélectionnée, l'icône par défaut sera utilisée.
toolbar.ordered_list: Liste ordonnée
oauth2.grant.moderate.magazine.ban.create: Bannir des utilisateurs dans vos
magazines modérés.
oauth2.grant.admin.user.delete: Supprimer des utilisateurs sur votre instance.
oauth.consent.app_requesting_permissions: souhaite effectuer les actions
suivantes en votre nom
oauth2.grant.moderate.post.change_language: Changer la langue des publications
dans vos magazines modérés.
oauth2.grant.moderate.magazine_admin.delete: Supprimer tous les magazines qui
vous appartiennent.
oauth2.grant.moderate.entry_comment.all: Modérer les commentaires dans les fils
de discussion de vos magazines modérés.
oauth2.grant.admin.magazine.move_entry: Déplacer les fils de discussions entre
les magazines de votre instance.
oauth2.grant.post_comment.edit: Modifier vos commentaires existants sur les
publications.
oauth2.grant.entry.create: Créer de nouveaux threads.
federated_search_only_loggedin: Recherche fédérée limitée si vous n'êtes pas
connecté
oauth2.grant.moderate.magazine.reports.action: Accepter ou refuser les rapports
dans vos magazines modérés.
oauth2.grant.admin.magazine.purge: Supprimer complètement les magazines de votre
instance.
your_account_is_not_active: Votre compte n'a pas été activé. Veuillez consulter
votre adresse e-mail pour obtenir des instructions d'activation de compte ou
demander un nouvel e-mail d'activation de compte.
oauth2.grant.user.notification.read: Afficher vos notifications, y compris les
notifications de message.
filter.adult.show: Afficher le contenu NSFW
oauth.consent.allow: Autoriser
oauth2.grant.magazine.block: Bloquer ou débloquer les magazines et afficher les
magazines que vous avez bloqués.
oauth2.grant.magazine.subscribe: S'abonner ou se désabonner aux magazines et
afficher les magazines auxquels vous êtes abonné.
oauth2.grant.admin.user.all: Bannir, vérifier ou supprimer complètement les
utilisateurs de votre instance.
oauth2.grant.post_comment.report: Signaler tout commentaire sur une publication.
oauth2.grant.magazine.all: S'abonner à des magazines ou les bloquer, puis
afficher les magazines auxquels vous êtes abonné ou que vous bloquez.
oauth2.grant.vote.general: Upvoter, downvoter ou booster les fils de discussion,
les publications ou les commentaires.
custom_css: CSS personnalisé
oauth2.grant.entry.vote: Upvoter, booster ou downvoter pour n'importe quel fil.
oauth2.grant.moderate.magazine_admin.all: Créer, modifier ou supprimer les
magazines qui vous appartiennent.
comment_reply_position_help: Afficher le formulaire de réponse aux commentaires
en haut ou en bas de la page. Lorsque le « défilement infini » est activé, la
position apparaîtra toujours en haut.
filter.adult.only: Uniquement le contenu NSFW
oauth2.grant.post_comment.vote: Upvoter, booster, ou downvoter tout commentaire
sur une publication.
filter.fields.names_and_descriptions: Les noms et descriptions
oauth2.grant.user.oauth_clients.read: Afficher les autorisations que vous avez
accordées à d'autres applications OAuth2.
oauth2.grant.moderate.entry.change_language: Changer la langue des fils de
discussion dans vos magazines modérés.
password_confirm_header: Confirmez votre demande de modification de mot de
passe.
block: Bloquer
oauth2.grant.moderate.all: Effectuer toute action de modération que vous êtes
autorisé à effectuer dans vos magazines modérés.
oauth2.grant.moderate.magazine.ban.all: Gérer les utilisateurs bannis dans vos
magazines modérés.
oauth2.grant.moderate.magazine.all: Gérer les bannissements, les rapports et
afficher les articles mis à la corbeille dans vos magazines modérés.
oauth2.grant.admin.federation.all: Afficher et mettre à jour les instances
actuellement défédérées.
toolbar.quote: Citation
oauth2.grant.user.notification.all: Afficher et effacer vos notifications.
oauth2.grant.report.general: Signaler des fils de discussion, des publications
ou des commentaires.
oauth2.grant.moderate.magazine.list: Afficher la liste de vos magazines modérés.
oauth2.grant.admin.post.purge: Supprimer complètement de votre instance toute
les publications.
oauth2.grant.moderate.magazine.ban.read: Afficher les utilisateurs bannis dans
vos magazines modérés.
oauth2.grant.user.profile.all: Afficher et modifier votre profil.
oauth2.grant.admin.user.ban: Bannir ou annuler le bannissement des utilisateurs
de votre instance.
show_avatars_on_comments: Afficher les avatars de commentaires
oauth2.grant.admin.all: Effectuer des action administrative sur votre instance.
toolbar.unordered_list: Liste non ordonnée
errors.server404.title: 404 Page introuvable
resend_account_activation_email_success: Si un compte associé à cet e-mail
existe, nous vous enverrons un nouvel e-mail d'activation.
errors.server403.title: 403 Accès réservé
oauth2.grant.post.report: Signaler n'importe quel message.
oauth2.grant.moderate.magazine.reports.read: Afficher les rapports dans vos
magazines modérés.
ignore_magazines_custom_css: Ignorer les CSS personnalisés des magazines
oauth2.grant.entry_comment.create: Créer de nouveaux commentaires dans les fils
de discussion.
oauth.consent.deny: Refuser
oauth2.grant.user.follow: Suivre ou ne plus suivre des utilisateurs, puis
afficher la liste des utilisateurs que vous suivez.
flash_post_pin_success: La publication a été épinglée avec succès.
oauth2.grant.entry_comment.report: Signaler tout commentaire dans un fil de
discussion.
moderation.report.approve_report_confirmation: Êtes-vous sûr de vouloir
approuver ce rapport ?
oauth2.grant.moderate.entry.all: Modérer les discussions dans vos magazines
modérés.
oauth.consent.title: Formulaire de consentement OAuth2
resend_account_activation_email: Renvoyer l'e-mail d'activation du compte
oauth2.grant.entry_comment.all: Créer, modifier ou supprimer vos commentaires
dans les fils de discussion, et voter, booster ou signaler tout commentaire
dans un fil de discussion.
oauth2.grant.entry_comment.delete: Supprimer vos commentaires existants dans les
fils de discussion.
subject_reported_exists: Ce contenu a déjà été signalé.
oauth2.grant.moderate.entry_comment.set_adult: Marquer les commentaires dans les
fils de discussion comme NSFW dans vos magazines modérés.
oauth2.grant.entry.edit: Modifier vos threads existants.
oauth2.grant.moderate.entry_comment.trash: Supprimer ou restaurer des
commentaires dans les fils de discussion de vos magazines modérés.
oauth2.grant.moderate.post.all: Modérer les publications dans vos magazines
modérés.
preferred_languages: Filtrer les langues des fils de discussion et des
publications
errors.server500.title: 500 Erreur du serveur
oauth2.grant.entry_comment.edit: Modifier vos commentaires existants dans les
fils de discussion.
oauth2.grant.moderate.magazine_admin.update: Modifier les règles, la
description, le statut NSFW ou l'icône de vos magazines propriétaires.
oauth2.grant.moderate.entry.trash: Mettre à la corbeille ou restaurer les fils
de discussion dans vos magazines modérés.
oauth2.grant.user.oauth_clients.all: Afficher et modifier les autorisations que
vous avez accordées à d'autres applications OAuth2.
oauth2.grant.user.profile.read: Afficher votre profil.
toolbar.link: Lien
oauth2.grant.admin.oauth_clients.read: Afficher les clients OAuth2 qui existent
sur votre instance et leurs statistiques d'utilisation.
toolbar.mention: Mention
oauth2.grant.write.general: Créez ou modifiez l'un de vos fils de discussion,
publications ou commentaires.
single_settings: Unique
oauth2.grant.moderate.post_comment.set_adult: Marquer les commentaires sur les
publications en tant que NSFW dans vos magazines modérés.
oauth2.grant.moderate.post_comment.change_language: Modifier la langue des
commentaires sur les publications dans vos magazines modérés.
moderation.report.ban_user_title: Bannir l'utilisateur
filter.origin.label: Choisissez l'origine
resend_account_activation_email_question: Compte inactif ?
oauth2.grant.admin.user.verify: Vérifier des utilisateurs sur votre instance.
resend_account_activation_email_description: Entrez l'adresse e-mail associée à
votre compte. Nous vous enverrons un autre e-mail d'activation.
reload_to_apply: Recharger la page pour appliquer les modifications
oauth2.grant.entry.delete: Supprimer vos threads existants.
your_account_has_been_banned: Votre compte a été banni
oauth2.grant.admin.instance.information.edit: Mettre à jour les pages À propos,
FAQ, Contact, Conditions d'utilisation et Politique de confidentialité de
votre instance.
oauth2.grant.read.general: Lisez tout le contenu auquel vous avez accès.
oauth2.grant.domain.all: Abonnez-vous à des domaines ou bloquez-les, et affichez
les domaines auxquels vous vous abonnez ou que vous bloquez.
toolbar.code: Code
errors.server500.description: Désolé, quelque chose s'est mal passé de notre
côté. Nous travaillons à résoudre ce problème, veuillez revenir bientôt.
oauth.client_not_granted_message_read_permission: Cette application n'a pas reçu
l'autorisation de lire vos messages.
restrict_oauth_clients: Restreindre la création de clients OAuth2 aux
administrateurs
flash_post_unpin_success: La publication a été désépinglée avec succès.
oauth2.grant.post_comment.delete: Supprimer vos commentaires existants sur les
publications.
bot_body_content: "Bienvenue dans le robot Mbin ! Ce robot joue un rôle crucial dans
l'activation de la fonctionnalité ActivityPub dans Mbin. Il garantit que Mbin peut
communiquer et fédérer avec d'autres instances dans le fediverse.\n\nActivityPub
est un protocole standard ouvert qui permet aux plateformes de réseaux sociaux décentralisées
de communiquer et d'interagir les unes avec les autres. Il permet aux utilisateurs
de différentes instances (serveurs) de se suivre, d'interagir et de partager du
contenu sur le réseau social fédéré connu sous le nom de fediverse. Il fournit aux
utilisateurs un moyen standardisé de publier du contenu, de suivre d'autres utilisateurs
et de participer à des interactions sociales telles que le fait d'aimer, de partager
et de commenter des fils de discussion ou des publications."
oauth2.grant.admin.oauth_clients.revoke: Révoquer l'accès aux clients OAuth2 sur
votre instance.
oauth2.grant.admin.instance.settings.edit: Mettre à jour les paramètres de votre
instance.
oauth2.grant.moderate.entry.set_adult: Marquer les fils de discussion comme NSFW
dans vos magazines modérés.
oauth2.grant.delete.general: Supprimez l'un de vos fils de discussion,
publications ou commentaires.
oauth2.grant.entry_comment.vote: Upvoter, booster, ou downvoter pour n'importe
quel commentaire dans un fil de discussion.
oauth2.grant.admin.instance.stats: Afficher les statistiques de votre instance.
oauth2.grant.admin.instance.settings.all: Afficher ou mettre à jour les
paramètres de votre instance.
oauth2.grant.entry.all: Créer, modifier ou supprimer vos fils de discussion, et
voter, booster ou signaler n'importe quel fil de discussion.
magazine_theme_appearance_background_image: Image d'arrière-plan personnalisée
qui sera appliquée lors de l'affichage du contenu de votre magazine.
unblock: Débloquer
oauth2.grant.admin.federation.read: Consulter la liste des instances défédérées.
oauth2.grant.moderate.entry_comment.change_language: Changer la langue des
commentaires dans les fils de discussion de vos magazines modérés.
oauth2.grant.moderate.magazine_admin.stats: Afficher le contenu, les votes et
les statistiques d'affichage des magazines qui vous appartiennent.
oauth.consent.grant_permissions: Accorder les autorisations
oauth2.grant.user.message.create: Envoyer des messages à d'autres utilisateurs.
oauth2.grant.admin.oauth_clients.all: Afficher ou révoquer les clients OAuth2
qui existent sur votre instance.
oauth2.grant.moderate.magazine.ban.delete: Annuler le bannissement
d'utilisateurs dans vos magazines modérés.
oauth.client_identifier.invalid: ID client OAuth non valide !
oauth2.grant.post_comment.all: Créer, modifier ou supprimer vos commentaires sur
les publications, et voter, booster ou signaler tout commentaire sur une
publication.
oauth2.grant.admin.user.purge: Supprimer complètement les utilisateurs de votre
instance.
update_comment: Mettre à jour le commentaire
infinite_scroll_help: Chargez automatiquement plus de contenu lorsque vous
atteignez le bas de la page.
oauth2.grant.user.notification.delete: Effacer vos notifications.
show_avatars_on_comments_help: Afficher/masquer les avatars des utilisateurs
lorsque vous affichez des commentaires sur un seul fil de discussion ou
publication.
oauth2.grant.post.all: Créer, modifier ou supprimer vos microblogs, et voter,
booster ou signaler n'importe quel microblog.
oauth.consent.app_has_permissions: peut déjà effectuer les actions suivantes
email.delete.title: Demande de suppression de compte utilisateur
oauth2.grant.block.general: Bloquer ou débloquer un magazine, un domaine ou un
utilisateur, et afficher les magazines, les domaines et les utilisateurs que
vous avez bloqués.
moderation.report.reject_report_confirmation: Êtes-vous sûr de vouloir rejeter
ce rapport ?
oauth2.grant.post_comment.create: Créer de nouveaux commentaires sur les
publications.
oauth2.grant.user.block: Bloquer ou débloquer des utilisateurs et afficher la
liste des utilisateurs que vous bloquez.
oauth2.grant.post.delete: Supprimer de nouvelles publications.
oauth2.grant.subscribe.general: S'abonner ou suivre n'importe quel magazine,
domaine ou utilisateur, et afficher les magazines, domaines et utilisateurs
auxquels vous êtes abonné.
oauth2.grant.moderate.post_comment.trash: Supprimer ou restaurer des
commentaires sur les publications de vos magazines modérés.
oauth2.grant.moderate.magazine_admin.moderators: Ajouter ou supprimer les
modérateurs de l'un de vos magazines propriétaires.
email_confirm_link_help: Vous pouvez également copier et coller ce qui suit dans
votre navigateur
oauth2.grant.admin.entry_comment.purge: Supprimer complètement de votre instance
tout les commentaires d'un fil de discussion.
toolbar.strikethrough: Barré
comment_reply_position: Position de la réponse au commentaire
oauth2.grant.post.create: Créer de nouvelles publications.
toolbar.image: Image
oauth2.grant.domain.block: Bloquer ou débloquer des domaines et afficher les
domaines que vous avez bloqués.
oauth2.grant.admin.instance.all: Afficher et mettre à jour les paramètres ou les
informations de l'instance.
sticky_navbar_help: La barre de navigation se collera en haut de la page lorsque
vous faites défiler vers le bas.
toolbar.italic: Italique
more_from_domain: Plus du domaine
oauth2.grant.user.profile.edit: Modifier votre profil.
oauth2.grant.admin.post_comment.purge: Supprimer complètement de votre instance
tout commentaire sur une publication.
oauth2.grant.moderate.magazine_admin.badges: Créer ou supprimer des badges des
magazines qui vous appartiennent.
oauth2.grant.moderate.post.pin: Épinglez des publications en haut de vos
magazines modérés.
user_badge_bot: Robot
suspend_account: Suspendre le compte
user_badge_op: OP
user_badge_admin: Admin
announcement: Annonce
show: Afficher
hide: Masquer
back: Retour
version: Version
admin_users_active: Actifs
2fa.authentication_code.label: Code d'authentification
related_entry: Connexes
and: et
edited: édité
auto: Auto
delete_magazine: Supprimer le magazine
purge_magazine: Purger le magazine
purge_content: Purger le contenu
delete_content: Supprimer le contenu
subscription_panel_large: Grand panneau
page_width: Largeur de la page
page_width_max: Max
restore_magazine: Restaurer le magazine
deletion: Suppression
accept: Accepter
action: Action
keywords: Mots-clés
details: Détails
sensitive_warning: Contenu sensible
from: de
tag: Étiquette
someone: Quelqu'un
default_theme: Thème par défaut
2fa.remove: Supprimer 2FA
menu: Menu
abandoned: Abandonné
cancel_request: Annuler la demande
show_subscriptions: Afficher les abonnements
alphabetically: Alphabétiquement
page_width_auto: Auto
page_width_fixed: Fixe
user_badge_moderator: Mod
reported: signalé
report_subject: Sujet
unsuspend_account: Annuler la suspension du compte
sort_by: Trier par
filter_by_subscription: Filtrer par abonnement
filter_by_federation: Filtrer par statut de fédération
close: Fermer
pending: En attente
position_bottom: En bas
position_top: En haut
two_factor_authentication: Authentification à deux facteurs
admin_users_suspended: Suspendus
admin_users_banned: Bannis
admin_users_inactive: Inactifs
enabled: Activé
disabled: Désactivé
hidden: Masqué
magazine_deletion: Suppression de magazine
================================================
FILE: translations/messages.gl.yaml
================================================
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 nos temas
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: Temas ao chou
related_entries: Temas relacionados
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 do tema! Os comentarios ao
completo están dispoñibles na páxina da publicación.
mercure_enabled: Mercure activado
report_issue: Incidencias
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: Mostra vista previa do multimedia (foto, vídeo) a tamaño
maior debaixo do contido.
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: Non se activou a túa conta. Mira no 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 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
fose activada.
resend_account_activation_email_success: Se existe unha conta asociada a este
correo, enviaremos 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 calquera tema da túa
instancia.
oauth2.grant.read.general: Ler todo o contido ao que ti teñas acceso.
oauth2.grant.delete.general: Eliminar calquera dos teus temas, publicacións ou
comentarios.
oauth2.grant.report.general: Denunciar temas, publicacións ou comentarios.
oauth2.grant.vote.general: Voto positivo ou negativo, promover temas,
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 tema.
oauth2.grant.entry_comment.all: Crear, editar ou eliminar os teus comentarios en
temas, e votar, promover ou denunciar calquera comentario nun tema.
oauth2.grant.entry_comment.create: Crear novos comentarios en temas.
oauth2.grant.entry_comment.edit: Editar os teus comentarios existentes en temas.
oauth2.grant.entry_comment.delete: Eliminar os teus comentarios en temas.
oauth2.grant.entry_comment.vote: Voto positivo ou negativo, promoción de
calquera comentario nun tema.
oauth2.grant.entry_comment.report: Denunciar calquera comentario nun tema.
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 temas 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 os temas como NSFW nas revistas
que moderas.
oauth2.grant.moderate.entry_comment.all: Moderar comentarios nos temas das
revistas que moderas.
oauth2.grant.moderate.entry.trash: Eliminar ou restablecer temas nas revistas
que moderas.
oauth2.grant.moderate.entry_comment.change_language: Cambiar o idioma dos
comentarios nos temas das 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 temas 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 temas das 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 nun
tema 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 os temas 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 temas 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 as PMF, Sobre,
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.
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 os temas,
publicacións e comentarios de outras usuarias nos temas, publicacións e
comentarios creados.
purge_content_desc: Purgar completamente o contido da usuaria, incluíndo as
respostas doutras usuarias nos temas, publicacións e comentarios creados.
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: Tema
reputation: Reputación
note: Nota
writing: Ao escribir
users: Usuarias
content: Contido
dashboard: Taboleiro
contact_email: Correo 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 os temas 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 calquera dos teus temas,
publicacións ou comentarios.
oauth2.grant.entry.all: Crear, editar ou eliminar os teus temas, e votar,
promover ou denunciar calquera tema.
oauth2.grant.entry.create: Crear novos temas.
oauth2.grant.entry.edit: Editar os teus temas existentes.
oauth2.grant.entry.delete: Eliminar os túas temas existentes.
oauth2.grant.entry.vote: Voto positivo ou negativo e promocion de calquera tema.
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 dos temas nas
revistas que moderas.
oauth2.grant.moderate.entry.pin: Fixar temas nas revistas que moderas.
update_comment: Actualizar comentario
show_avatars_on_comments_help: Mostra/Oculta os avatares ao ver os comentarios
nun tema ú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: Tema
type.photo: Foto
type.video: Vídeo
type.smart_contract: Pregado intelixente
type.magazine: Revista
thread: Tema
threads: Temas
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: Votos a favor
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 correo
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: Cláusula de privacidade
fediverse: Fediverso
create_new_magazine: Crear unha nova revista
add_new_article: Engadir novo tema
about_instance: Sobre
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: Temas
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 certeza?
delete: Eliminar
edit_post: Editar publicación
edit_comment: Gardar cambios
moderate: Moderar
reason: Razó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 de comentarios en temas que iniciei
notify_on_new_entry_comment_reply: Respostas aos meus comentarios en calquera
tema
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: Novos temas (ligazóns ou artigos) nunha revista á que me
subscribín
about: Sobre
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 un tema de
restored_thread_by: restableceu un tema 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: vetada
he_unbanned: retirouse o 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: O tema creouse correctamente e xa é visible para
outras usuarias.
set_magazines_bar: Barra das revistas
flash_thread_edit_success: Editouse correctamente o tema.
flash_thread_delete_success: Eliminouse correctamente o tema.
flash_thread_pin_success: Fixouse correctamente o tema.
flash_thread_unpin_success: O tema xa non está fixado.
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 un novo tema
edited_thread: Editou un tema
mod_remove_your_thread: A moderación eliminou o teu tema
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 directa
message: Mensaxe
show_top_bar: Mostrar barra superior
sticky_navbar: Barra nav. fixa
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
type_search_term: Escribe termo a buscar
federation_enabled: A federación está activada
registrations_enabled: Permítese a creación de contas
account_deletion_button: Eliminar Conta
account_deletion_immediate: Eliminar inmediatamente
errors.server500.title: 500 Erro Interno do Servidor
private_instance: Forzar ás usuarias a iniciar sesión antes de poder ver
calquera contido
delete_account_desc: Eliminar a conta, incluíndo as respostas doutras usuarias
nos temas, publicacións e comentarios creados.
schedule_delete_account: Programar a eliminación
remove_schedule_delete_account: Desbotar a eliminación programada
remove_schedule_delete_account_desc: Borrar a eliminación programada. Todo o
contido volverá a estar dispoñible e a usuaria poderá iniciar sesión.
2fa.setup_error: Erro ao activar o 2FA para a conta
2fa.user_active_tfa.title: A usuaria ten o 2FA activo
password_and_2fa: Contrasinal e 2FA
flash_account_settings_changed: Cambiaronse correctamente os axustes da conta.
Debes volver a iniciar sesión.
show_subscriptions: Mostrar subscricións
subscription_sort: Orde
sidebars_same_side: En barras laterais no mesmo lado
subscription_sidebar_pop_out_right: Mover a unha barra lateral separada na
dereita
subscription_sidebar_pop_out_left: Mover a unha barra lateral separada na
esquerda
errors.server500.description: Desculpa, pero algo fallou pola nosa parte. Se
continúas a ver este erro intenta contactar coa administración da instancia.
Se a instancia non funciona en absoluto entón mira %link_start%outras
instancias Mbin%link_end% ata que resolvamos o problema.
schedule_delete_account_desc: Programa a eliminación desta conta en 30 días. Así
ocultarás á usuaria e aos seus contidos e evitarás que a usuaria poida iniciar
sesión.
alphabetically: Alfabético
subscriptions_in_own_sidebar: Nunha barra lateral separada
account_deletion_description: Vaise eliminar a túa conta en 30 días a non ser
que ti a elimines antes. A conta non pode restablecerse unha vez sexa
eliminada. Para restablecer a conta durante eses 30 días accede coas
credenciais habituais ou contacta coa administración.
flash_comment_new_success: Creouse correctamente o comentario.
flash_user_settings_general_success: Gardáronse correctamente os axustes como
usuaria.
unsuspend_account: Reactivar conta
remove_user_avatar: Retirar avatar
edit_entry: Editar tema
notify_on_user_signup: Novas contas
viewing_one_signup_request: Só estás a ver unha solicitude de conta de
%username%
close: Fechar
user_badge_moderator: Mod
announcement: Anuncio
continue_with: Continuar con
show_active_users: Mostrar usuarias activas
signup_requests_header: Solicitudes de contas
remove_user_cover: Retirar portada
enabled: Activado
bot_body_content: "Benvida ao Mbin Agent! Este axente ten un rol moi importante ao
activar as características ActivityPub de Mbin. Fai que Mbin se poida comunicar
e federar con outras instancias do Fediverso.\n\nActivityPub é un protocolo aberto
estandarizado que permite a plataformas de redes sociais descentralizadas comunicarse
e interactuar unhas con outras. Permite que persoas usuarias en diferentes instancias
(servidores) poidan seguirse, interactuar, e compartir contidos na rede social federada
coñecida como o Fediverso. Proporciona un xeito estandarizado para que as usuarias
publiquen contidos, se sigan entre si e se relacionen compartindo, gustando e comentando
nos temas ou publicacións."
oauth2.grant.user.bookmark: Engadir e retirar marcadores
subscription_panel_large: Taboleiro grande
subscription_header: Subscricións a revistas
position_bottom: Abaixo
position_top: Arriba
flash_image_download_too_large_error: Non se puido crear a imaxe, é demasiado
grande (tam. máx %bytes%)
flash_magazine_theme_changed_success: Actualizouse correctamente a aparencia da
revista.
flash_email_was_sent: Enviouse correctamente o correo.
flash_email_failed_to_sent: Non se enviou o correo.
flash_post_new_error: Non se creou a publicación. Algo fallou.
flash_comment_edit_success: Actualizouse correctamente o comentario.
flash_comment_edit_error: Fallou a edición do comentario. Algo fallou.
flash_user_settings_general_error: Non se gardaron os axustes como usuaria.
flash_user_edit_profile_error: Non se gardaron os axustes do perfil.
flash_user_edit_profile_success: Gardáronse os axustes do perfil da usuaria.
change_my_cover: Cambiar a miña portada
edit_my_profile: Editar o meu perfil
account_banned: Vetouse a conta.
account_is_suspended: A conta da usuaria está suspendida.
keywords: Palabras clave
sso_registrations_enabled: Activados os rexistros tipo SSO
restrict_magazine_creation: Restrinxir a creación de revistas locais á
administración e moderadoras globais
sso_show_first: Mostrar primeiro SSO no acceso e páxinas para crear contas
reported_user: Usuaria denunciada
own_content_reported_accepted: Aceptouse unha denuncia sobre o teu contido.
magazine_log_mod_removed: retirou a unha persoa da moderación
direct_message: Mensaxe directa
manually_approves_followers: Aproba manualmente as seguidoras
register_push_notifications_button: Rexistrar para notificacións Push
notification_title_mention: Mencionáronte
notification_title_new_signup: Rexistrouse unha nova conta
notification_body2_new_signup_approval: Debes aprobar a solicitude para que
poida acceder
show_related_magazines: Mostrar revistas ao chou
show_related_entries: Mostrar temas ao chou
notification_title_new_report: Creouse unha nova denuncia
flash_posting_restricted_error: A creación de temas nesta revista está
restrinxida á moderación e ti non pertences a ela
server_software: Software do servidor
version: Versión
last_successful_deliver: Última entrega correcta
bookmark_remove_from_list: Retirar marcador de %list%
admin_users_active: Activas
max_image_size: Tamaño máximo do ficheiro
bookmark_remove_all: Retirar todos os marcadores
bookmark_lists: Listas de marcadores
bookmark_add_to_list: Engadir marcador a %list%
bookmarks: Marcadores
bookmarks_list: Marcadores en %lista%
count: Número
is_default: Por defecto
bookmark_list_is_default: É a lista por defecto
bookmark_list_make_default: Converter en Por defecto
bookmark_list_selected_list: Lista seleccionada
table_of_contents: Táboa do contido
search_type_all: Todos os tipos
search_type_entry: Temas
search_type_post: Microblogs
signup_requests: Solicitudes de novas contas
application_text: Explica por que queres unirte
flash_application_info: A administración ten que aprobar a túa conta para que
poidas iniciar sesión. Vas recibir un correo cando a solicitude se tramite.
email_application_approved_title: Aprobouse a túa solicitude dunha nova conta
signup_requests_paragraph: Estas usuarias queren unirse ao teu servidor. Non
poden iniciar sesión ata que ti aprobes a súa solicitude.
email_verification_pending: Tes que verificar o enderezo de correo para poder
iniciar sesión.
show_new_icons: Mostrar novas iconas
email_application_approved_body: A administración do servidor aprobou a túa nova
conta. Podes acceder ao servidor en %siteName% .
email_application_rejected_title: Rexeitouse a solicitude dunha nova conta
email_application_pending: A conta precisa aprobación pola administración para
iniciar sesión.
show_new_icons_help: Mostra a icona para novas revistas/usuarias (30 días ou
máis recente)
magazine_log_mod_added: engadiu unha persoa á moderación
flash_thread_tag_banned_error: Non se puido crear o tema. O contido non está
permitido.
image_lightbox_in_list: As miniaturas dos temas abren a pantalla completa
compact_view_help: Unha vista compacta con marxes menores, e o multimedia móvese
á dereita.
show_users_avatars_help: Mostrar imaxe do avatar da usuaria.
show_magazines_icons_help: Mostrar a icona da revista.
show_thumbnails_help: Mostrar miniaturas.
image_lightbox_in_list_help: Ao marcar, cando premes nunha miniatura aparece
unha xanela modal coa imaxe. Se non, ao premer na miniatura abres o tema.
your_account_is_not_yet_approved: Aínda non se aprobou a túa conta.
Enviarémosche un correo tan pronto como a administración xestione a túa
solicitude dunha conta.
federation_page_dead_title: Instancias defuntas
federation_page_dead_description: Instancias ás que non puidemos entregarlle 10
actividades seguidas e que a última entrega ou recepción correcta se fixo hai
máis dunha semana
flash_post_new_success: Creouse correctamente a publicación.
flash_thread_edit_error: Fallou a edición do tema. Algo foi mal.
flash_user_edit_password_error: Fallou o cambio do contrasinal.
flash_post_edit_error: Fallou a edición da publicación. Algo foi mal.
page_width: Anchura da páxina
page_width_max: Máx
page_width_auto: Auto
account_unsuspended: Reactivouse a conta.
sensitive_show: Preme para mostrar
deleted_by_moderator: A moderación eliminou o tema, publicación ou comentario
notification_title_edited_comment: Editouse un comentario
notification_title_message: Nova mensaxe directa
notification_title_new_post: Nova publicación
notification_title_removed_post: Eliminouse unha publicación
notification_title_edited_post: Editouse unha publicación
last_successful_receive: Última recepción correcta
last_failed_contact: Último contacto fallado
user_verify: Activar conta
bookmark_list_create_label: Nome da lista
bookmarks_list_edit: Editar lista de marcadores
select_user: Elixe unha usuaria
new_users_need_approval: A administración ten que aprobar as novas usuarias
antes de que poidan iniciar sesión.
and: e
oauth2.grant.user.bookmark.add: Engadir marcadores
oauth2.grant.user.bookmark.remove: Retirar marcadores
oauth2.grant.user.bookmark_list: Ler, editar e eliminar as túas listas de
marcadores
oauth2.grant.user.bookmark_list.read: Ler as túas listas de marcadores
oauth2.grant.user.bookmark_list.edit: Editar a túas listas de marcadores
oauth2.grant.user.bookmark_list.delete: Eliminar as túas listas de marcadores
flash_comment_new_error: Fallo ao comentar. Algo fallou.
user_badge_op: AO
user_badge_admin: Admin
user_badge_global_moderator: Mod Glob
user_badge_bot: Robot
open_report: Abrir denuncia
report_accepted: Aceptouse unha denuncia
unregister_push_notifications_button: Retirar o rexistro de Push
notification_title_new_thread: Novo tema
test_push_message: Ola Mundo!
notification_title_new_comment: Novo comentario
notification_title_removed_comment: Eliminouse un comentario
notification_title_removed_thread: Eliminouse un tema
notification_title_edited_thread: Editouse un tema
notification_title_ban: Vetáronte
notification_body_new_signup: A usuaria %s% creou a conta.
magazine_posting_restricted_to_mods_warning: Só a moderación pode crear temas
nesta revista
magazine_posting_restricted_to_mods: Restrinxir a creación de temas á moderación
new_user_description: Esta é unha nova usuaria (activa hai menos de %s% días)
new_magazine_description: A revista é nova (activa desde hai menos de %days%
días)
bookmark_list_create: Crear
bookmark_list_create_placeholder: escribe un nome...
bookmark_list_edit: Editar
email_application_rejected_body: Grazas polo teu interese, pero lamentamos
informarte de que a túa petición foi rexeitada.
show_magazine_domains: Mostrar dominios das revistas
show_user_domains: Mostrar dominios das usuarias
answered: con resposta
by: por
front_default_sort: Orde na páxina de inicio
comment_default_sort: Orde por defecto dos comentarios
open_signup_request: Abrir petición de nova conta
downvotes_mode: Modo votos negativos
change_downvotes_mode: Cambiar o modo dos votos negativos
disabled: Desactivado
hidden: Oculto
magazine_log_entry_pinned: entrada fixada
magazine_log_entry_unpinned: eliminouse a entrada fixada
last_updated: Última actualización
show_related_posts: Mostrar publicacións ao chou
comments_count: '{0}Comentarios|{1}Comentario|]1,Inf[ Comentarios'
subscribers_count: '{0}Subscritoras|{1}Subscritora|]1,Inf[ Subscritoras'
followers_count: '{0}Seguidoras|{1}Seguidora|]1,Inf[ Seguidoras'
marked_for_deletion_at: Marcado para eliminación o %date%
disconnected_magazine_info: Esta revista non se está actualizando (última
actividade hai %days% día(s)).
subscription_sidebar_pop_in: Mover as subscricións ao taboleiro
flash_thread_new_error: Non se puido crear o tema. Algo fallou.
flash_magazine_theme_changed_error: Non se actualizou a aparencia da revista.
flash_post_edit_success: Editouse correctamente a publicación.
filter_labels: Filtrar etiquetas
auto: Auto
change_my_avatar: Cambiar o avatar
all_time: Sempre
show: Mostrar
sso_registrations_enabled.error: A creación de novas contas usando xestores de
identidades alleos está desactivada actualmente.
toolbar.spoiler: Velado
action: Acción
sso_only_mode: Restrinxir o acceso e creación de contas só a métodos SSO
related_entry: Relacionado
reporting_user: Denunciando usuaria
reported: denunciada
report_subject: Asunto
own_report_rejected: Rexeitouse a túa denuncia
own_report_accepted: Aceptouse a túa denuncia
cake_day: Aniversario
someone: Alguén
back: Atrás
test_push_notifications_button: Comprobar notificacións Push
bookmark_add_to_default_list: Engadir marcador á lista por defecto
comment_not_found: Non se atopa o comentario
sensitive_hide: Preme para ocultar
details: Detalles
spoiler: Velado
hide: Ocultar
edited: editado
page_width_fixed: Fixa
flash_user_edit_email_error: Fallou o cambio do correo.
account_unbanned: Retirouse o veto á conta.
sensitive_warning: Contido sensible
sensitive_toggle: Cambiar a visibilidade do contido sensible
deleted_by_author: A persoa autora eliminou o tema, publicación ou comentario
notification_title_new_reply: Nova resposta
admin_users_inactive: Inactivas
admin_users_suspended: Suspendidas
admin_users_banned: Vetadas
2fa.manual_code_hint: Se non podes escanear o código QR escribe a chave secreta
manualmente
toolbar.emoji: Emoji
magazine_instance_defederated_info: A instancia desta revista foi desfederada. A
revista non vai recibir actualizacións.
user_instance_defederated_info: A instancia desta usuaria foi desfederada.
flash_thread_instance_banned: A instancia desta revista foi vetada.
show_rich_mention: Mencións melloradas
show_rich_mention_help: Mostrar datos da usuaria cando é mencionada. Inclúe o
seu nome público e imaxe de perfil.
show_rich_mention_magazine: Mencións melloradas de revistas
show_rich_mention_magazine_help: Mostra datos da revista cando se menciona. Isto
inclúe o seu nome público e icona.
show_rich_ap_link: Ligazóns AP melloradas
show_rich_ap_link_help: Mostra información en liña cando se inclúe a ligazón a
contidos ActivityPub.
attitude: Actitude
type_search_term_url_handle: Escribe a palabra, url ou alcume a buscar
search_type_magazine: Revistas
search_type_user: Usuarias
search_type_actors: Revistas + Usuarias
search_type_content: Temas + Microblogs
type_search_magazine: Limitar busca á revista…
type_search_user: Limitar a busca á autoría…
modlog_type_entry_deleted: Fío eliminado
modlog_type_entry_restored: Fío restaurado
modlog_type_entry_comment_deleted: Comentario do fío eliminado
modlog_type_entry_comment_restored: Comentario do fío restaurado
modlog_type_entry_pinned: Fío fixado
modlog_type_entry_unpinned: Fío solto
modlog_type_post_deleted: Microblog eliminado
modlog_type_post_restored: Microblog restaurado
modlog_type_post_comment_deleted: Resposta do microblog eliminada
modlog_type_post_comment_restored: Resposta do microblog restaurada
modlog_type_ban: Usuaria expulsada da revista
modlog_type_moderator_add: Engadiuse unha moderadora da revista
modlog_type_moderator_remove: Moderadora da revista retirada
everyone: Todo o mundo
nobody: Ninguén
followers_only: Só seguidoras
direct_message_setting_label: Quen pode enviarche unha mensaxe directa
banner: Imaxe cabeceira
magazine_theme_appearance_banner: Imaxe de cabeceira persoal para a revista.
Móstrase na parte superior de todos os fíos e debería ter proporcións anchas
(5:1, ou 1500px * 300px).
delete_magazine_icon: Eliminar icona da revista
flash_magazine_theme_icon_detached_success: Eliminouse correctamente a icona da
revista
delete_magazine_banner: Eliminar cabeceira da revista
flash_magazine_theme_banner_detached_success: Eliminouse correctamente a imaxe
de cabeceira
flash_thread_ref_image_not_found: Non se puido atopar a imaxe referenciada por
'imageHash'.
federation_uses_allowlist: Usa lista de permitidos para a federación
defederating_instance: Deixando de federar con %i
their_user_follows: Cantidade de usuarias desa instancia que seguen a usuarias
da túa instancia
our_user_follows: Cantidade de usuarias da nosa instancia que seguen usuarias da
outra instancia
their_magazine_subscriptions: Cantidade de usuarias da outra instancia
subscritas a revistas da nosa instancia
our_magazine_subscriptions: Cantidade de usuarias da nosa instancia subscritas a
revistas da outra instancia
confirm_defederation: Confirmar a desfederación
flash_error_defederation_must_confirm: Tes que confirmar a desfederación
allowed_instances: Instancias permitidas
btn_deny: Rexeitar
btn_allow: Permitir
ban_instance: Vetar instancia
allow_instance: Permitir instancia
federation_page_use_allowlist_help: Se utilizas unha lista de permitidas, esta
instancia só federará coas que estén incluídas nesa lista. Se non é así, esta
instancia federará con todas as instancias excepto as que estean vetadas.
crosspost: Compartir fóra
ban_expires: O veto caduca
you_have_been_banned_from_magazine: Vetáronte na revista %m.
you_have_been_banned_from_magazine_permanently: Vetáronte permanentemente na
revista %m.
you_are_no_longer_banned_from_magazine: Xa non estás vetada na revista %m.
front_default_content: Vista por defecto da páxina inicial
default_content_default: Por defecto (Fíos)
default_content_combined: Fíos + Microblog
default_content_threads: Fíos
default_content_microblog: Microblog
combined: Combinada
sidebar_sections_random_local_only: Limitar as seccións do panel lateral a
"Fíos/Publicacións ao chou" só locais
sidebar_sections_users_local_only: Limitar a sección "Persoas activas" do panel
lateral a só locais
random_local_only_performance_warning: Activar "Ao chou só local" podería
impactar no rendemento SQL.
oauth2.grant.moderate.entry.lock: Bloquea os fíos nas revistas que moderas, polo
que ninguén poderá comentar neles
oauth2.grant.moderate.post.lock: Bloquea os microblogs nas revistas que moderas,
polo que ninguén poderá comentalos
discoverable: Descubrible
user_discoverable_help: Se activas isto poderase atopar nas buscas e paneis ao
chou tanto o teu perfil, fíos, microblogs e comentarios. O teu perfil tamén
vai aparecer no panel de usuarias activas e na páxina de persoas. Se o
desactivas, as túas publicacións seguirán sendo visibles por outras usuarias,
pero non se mostrarán na cronoloxía global.
flash_thread_lock_success: Fío bloqueado correctamente
flash_thread_unlock_success: Fío desbloqueado correctamente
flash_post_lock_success: Microblog bloqueado correctamente
flash_post_unlock_success: Microblog desbloqueado correctamente
lock: Bloquear
unlock: Desbloquear
magazine_discoverable_help: Se activas isto poderanse atopar esta revista e
fíos, microblogs e comentarios desta revista nas buscas e paneis ao chou. Se
está desactivado a revista aparecerá igualmente na lista de revistas, pero os
fíos e microblogs non aparecerán na cronoloxía global.
comments_locked: Os comentarios están bloqueados.
magazine_log_entry_locked: bloqueou os comentarios en
magazine_log_entry_unlocked: desbloqueou os comentarios en
modlog_type_entry_lock: Fío bloqueado
modlog_type_entry_unlock: Fío desbloqueado
modlog_type_post_lock: Microblog bloqueado
modlog_type_post_unlock: Microblog desbloqueado
contentnotification.muted: Silenciar | non ter notificacións
contentnotification.default: Predeterminado | recibir notificacións seguindo os
axustes
contentnotification.loud: Ruidoso | notificar todo
indexable_by_search_engines: Os motores de busca poden atopalo
user_indexable_by_search_engines_help: Se este axuste se desactiva, os motores
de busca reciben o sinal de non incluír ningún dos teus fíos ou microblogs,
pero os comentarios non se ven afectados e actores maliciosos poderían
ignoralo. Este axuste fedérase con outros servidores.
magazine_indexable_by_search_engines_help: Se este axuste se desactiva, os
motores de busca reciben o aviso de non incluír non resultados ningún dos fíos
ou microblogs nestas revistas. Isto inclúe a páxina de inicio e todas as
páxinas de comentarios. Este axuste tamén se federa con outros servidores.
magazine_name_as_tag: Usar o nome da revista como etiqueta
magazine_name_as_tag_help: As etiquetas dunha revista úsanse para ligar as
publicacións microblog a esta revista. Por exemplo, se o nome é «fediverso» e
a revista ten a etiqueta «fediverso», entón todas as publicacións microblog
que conteñan «#fediverso» vanse incluír nesta revista.
magazine_rules_deprecated: o campo coas regras xa non se usa, e vaise retirar no
futuro. Pon as túas regras na cadro coa descrición.
================================================
FILE: translations/messages.gsw.yaml
================================================
add: Dezuefüege
filter_by_subscription: Nach Abonnemänt filterä
type.article: Thema
type.smart_contract: Smart Contract
type.magazine: Magazin
thread: Thema
threads: Themä
microblog: Mikroblog
people: Lüüt
events: Ereignissä
magazine: Magazin
magazines: Magazinä
search: Suechä
select_channel: Wähl en Kanal
login: Ahmeldä
top: Top
hot: Heiss
active: Aktiv
newest: Neu
oldest: Alt
commented: Kommentiert
change_view: Ahsicht wächsle
filter_by_time: Nach de Ziit filterä
filter_by_type: Nach em Typ filterä
filter_by_federation: Nach Föderierigsstatus filterä
comments_count: '{0}Kommentär|{1}Kommentar|]1,Inf[ Kommentär'
followers_count: '{0}Follower|{1}Follower|]1,Inf[ Follower'
marked_for_deletion: Für Löschig markiert
marked_for_deletion_at: Für Löschig am %date% markiert
more: Meh
avatar: Avatar
added: Dezuegfüegt
up_votes: Förderä
favourite: Duume ufä
register: Registrierä
reset_password: Passwort zrüggsetzä
in: i
from: vo
username: Bnutzername
email: E-Mail
repeat_password: Passwort wiederholä
type.link: Link
type.video: Video
type.photo: Foti
sort_by: Sortierä nach
subscribers_count: '{0}Abonnänte|{1}Abonnänt|]1,Inf[ Abonnänte'
favourites: Favoritä
show_more: Meh zeige
already_have_account: Hesch scho es Konto?
to: ah
================================================
FILE: translations/messages.it.yaml
================================================
type.article: Argomento
type.photo: Foto
type.video: Video
type.smart_contract: Contratto Smart
type.magazine: Rivista
thread: Argomento
people: Persone
events: Eventi
magazine: Rivista
magazines: Riviste
search: Cerca
add: Aggiungi
select_channel: Seleziona un canale
threads: ArgomentiThread
hot: Popolari
active: Attivi
newest: Più recenti
oldest: Più vecchi
commented: Più commentati
change_view: Cambia vista
comments_count: '{0}Commenti|{1}Commento|]1,Inf[ Commenti'
favourite: Preferito
more: Altro
avatar: Avatar
added: Aggiunto
up_votes: Boost
down_votes: Voti negativi
created_at: Creato
owner: Proprietario
online: Online
comments: Commenti
posts: Post
replies: Risposte
markdown_howto: Come funziona l'editor?
moderators: Moderatori
add_comment: Aggiungi commento
add_post: Aggiungi post
add_media: Aggiungi media
enter_your_comment: Inserisci il tuo commento
enter_your_post: Inserisci il tuo post
activity: Attività
cover: Copertina
related_posts: Post correlati
random_posts: Post casuali
go_to_original_instance: Sfoglia più contenuti sull'istanza originale.
empty: Vuoto
subscribe: Iscriviti
unsubscribe: Annulla iscrizione
follow: Segui
reply: Rispondi
login_or_email: Accedi con il nome utente o l'e-mail
dont_have_account: Non hai ancora un account?
already_have_account: Hai già un account?
password: Password
remember_me: Ricordami
reset_password: Reimposta password
show_more: Mostra di più
email_confirm_content: 'Sei pronto ad attivare il tuo account Mbin? Clicca sul link
sottostante:'
reset_check_email_desc2: Se non vedi alcuna e-mail, controlla nella cartella
della posta indesiderata.
email_confirm_expire: Ricorda che il link scade tra un ora.
email_confirm_header: Ciao! Conferma il tuo indirizzo email.
subscriptions: Iscrizioni
subscribed: Iscritto
notify_on_new_posts: Avvisami di nuovi post nelle riviste a cui sono iscritto
notify_on_new_post_comment_reply: Avvisami in caso di risposte ai commenti dei
miei post
notify_on_new_entry_reply: Avvisami in caso di commenti nei miei thread
notify_on_new_post_reply: Avvisami in caso di risposte ai miei post
show_profile_subscriptions: Mostra le iscrizioni alla rivista
flash_thread_new_success: Il thread è stato creato con successo ed è ora
visibile agli altri utenti.
set_magazines_bar_desc: aggiungi il nome della rivista dopo la virgola
flash_thread_delete_success: Il thread è stato eliminato con successo.
flash_thread_unpin_success: Il thread è stato rimosso dai fissati in evidenza
con successo.
flash_magazine_edit_success: La rivista è stata modificata con successo.
too_many_requests: Limite superato, riprova più tardi.
mod_remove_your_thread: Un moderatore ha rimosso il tuo thread
add_mentions_posts: Aggiungi tag di menzione degli utenti nei post
Your account is not active: Il tuo account non è attivo.
Your account has been banned: Il tuo account è stato sospeso.
magazine_panel_tags_info: Da inserire solo se si desidera che i contenuti del
fediverso vengano aggiunti a questa rivista in base ai tag
browsing_one_thread: Stai visualizzando solo uno dei thread nella discussione!
Nella pagina del post specifico potrai trovare tutti commenti.
kbin_intro_desc: è una piattaforma decentralizzata per l'aggregazione di
contenuti e microblogging che opera all'interno della rete del Fediverso.
kbin_promo_desc: '%link_start%Clona il repository%link_end% e sviluppa il fediverso'
type.link: Link
login: Accedi
register: Registrati
flash_register_success: "Benvenuto a bordo. Il tuo account è stato registrato. Un
ultimo passo, controlla la tua casella email per il link di attivazione del tuo
account."
microblog: Microblog
top: Migliori
filter_by_time: Filtra per data
type_search_term: Digita un termine di ricerca
favourites: voti positivi
unfollow: Non seguire
you_cant_login: Hai dimenticato la password?
reset_check_email_desc: Se esiste un account associato al tuo indirizzo email,
dovresti a breve ricevere un'email contenente un link che puoi usare per
reimpostare la tua password. Tra %expire% il link non sarà più attivo.
flash_magazine_new_success: La rivista è stata creata con successo. Ora puoi
aggiungere nuovi contenuti o esplorare il pannello di amministrazione delle
riviste.
mod_log: Registro di moderazione
mod_log_alert: ATTENZIONE - Il registro di moderazione potrebbe contenere
contenuti non piacevoli o angoscianti che sono stati rimossi dai moderatori.
Per favore procedi con cautela.
set_magazines_bar_empty_desc: se il campo è vuoto, nella barra vengono
visualizzate le riviste attive.
federated_magazine_info: 'La rivista appartiene a un server federato e potrebbe essere
incompleta.'
federated_user_info: 'Il profilo appartiene a un server federato e potrebbe essere
incompleto.'
notify_on_new_entry: Avvisami di nuovi thread nelle riviste a cui sono iscritto
agree_terms: Acconsenti ai %terms_link_start%Termini e
condizioni%terms_link_end% e all'%policy_link_start%Informativa sulla
privacy%policy_link_end%
notify_on_new_entry_comment_reply: Avvisami in caso di risposte ai commenti dei
miei thread
flash_thread_edit_success: Il thread è stato modificato con successo.
flash_thread_pin_success: Il thread è stato fissato in evidenza con successo.
mod_deleted_your_comment: Un moderatore ha cancellato il tuo commento
mod_remove_your_post: Un moderatore ha rimosso il tuo post
add_mentions_entries: Aggiungi tag di menzione degli utenti nei contenuti
email_confirm_title: Conferma il tuo indirizzo email.
copy_url_to_fediverse: Copia l'url univoco nel fediverso
no_comments: Non ci sono commenti
subscribers: Iscritti
filter_by_type: Filtra per tipo
to: a
in: in
username: Nome utente
email: Email
repeat_password: Ripeti la password
terms: Termini di servizio
privacy_policy: Politica della privacy
about_instance: A proposito
all_magazines: Tutte le riviste
stats: Statistiche
fediverse: Fediverso
create_new_magazine: Crea una nuova rivista
add_new_article: Aggiungi un nuovo thread
add_new_link: Aggiungi un nuovo link
add_new_photo: Aggiungi una nuova foto
add_new_post: Aggiungi un nuovo post
add_new_video: Aggiungi un nuovo video
contact: Contatti
faq: FAQ
rss: RSS
change_theme: Cambia tema
useful: Utile
help: Aiuto
try_again: Riprova
down_vote: Riduci
email_verify: Conferma l’indirizzo email
select_magazine: Seleziona una rivista
add_new: Aggiungi nuovo
url: URL
title: Titolo
tags: Etichette
badges: Distintivi
is_adult: 18+ / Contenuti espliciti
eng: ENG
oc: OC
image: Immagine
image_alt: Testo alternativo per l’immagine
name: Nome
description: Descrizione
rules: Regole
domain: Dominio
followers: Seguaci
following: Seguiti
overview: Panoramica
cards: Carte
columns: Colonne
user: Utente
moderated: Moderato
people_local: Locale
people_federated: Federato
reputation_points: Punti reputazione
related_tags: Etichette correlate
go_to_content: Vai al contenuto
go_to_search: Vai alla ricerca
all: Tutto
logout: Esci
classic_view: Vista classica
compact_view: Vista compatta
chat_view: Vista chat
tree_view: Vista ad albero
cards_view: Vista a schede
3h: 3 ore
6h: 6 ore
12h: 12 ore
1d: 1 giorno
1w: 1 settimana
1m: 1 mese
1y: 1 anno
links: Collegamenti
articles: Thread
photos: Foto
report: Rapporto
share: Condividi
copy_url: Copia l’url
share_on_fediverse: Condividi sul Fediverso
edit: Modifica
are_you_sure: Confermi?
reason: Motivo
delete: Cancella
edit_post: Modifica il post
edit_comment: Modifica il commento
settings: Preferenze
general: Generale
profile: Profilo
blocked: Bloccati
reports: Rapporti
messages: Messaggi
appearance: Aspetto
homepage: Homepage
hide_adult: Nascondi contenuto esplicito
privacy: Privacy
show_profile_followings: Mostra gli utenti seguaci
save: Salva
about: A proposito
old_email: Email attuale
new_email: Nuova email
new_email_repeat: Conferma nuova email
current_password: Password attuale
new_password: Nuova password
new_password_repeat: Conferma la nuova password
change_email: Modifica email
change_password: Modifica password
expand: Espandi
collapse: Riduci
domains: Domini
error: Errore
votes: Voti
dark: Scuro
light: Chiaro
solarized_light: Solarized Light
solarized_dark: Solarized Dark
font_size: Dimensione carattere
size: Dimensione
boosts: Boost
yes: Si
no: No
show_magazines_icons: Mostra le icone delle riviste
show_thumbnails: Mostra le miniature
rounded_edges: Angoli arrotondati
removed_thread_by: ha rimosso un thread di
restored_thread_by: ha ripristinato un thread di
restored_comment_by: ha ripristinato un commento di
removed_post_by: ha rimosso un post di
restored_post_by: ha ripristinato un post di
read_all: Leggi tutto
show_all: Mostra tutto
Password is invalid: La password non è valida.
check_email: Controlla la tua email
joined: Iscritto
go_to_filters: Vai ai filtri
table_view: Vista a tabella
videos: Video
notifications: Notifiche
theme: Tema
show_users_avatars: Mostra gli avatar degli utenti
removed_comment_by: ha rimosso un commento di
body: Contenuto
up_vote: Boost
moderate: Moderare
featured_magazines: Riviste in primo piano
he_banned: sospendi
he_unbanned: rimuovi sospensione
set_magazines_bar: Barra delle riviste
added_new_thread: Ha aggiunto un nuovo thread
edited_thread: Ha modificato un thread
added_new_comment: Ha aggiunto un nuovo commento
edited_comment: Ha modificato un commento
replied_to_your_comment: Ha risposto al tuo commento
added_new_post: Ha aggiunto un nuovo post
wrote_message: Ha scritto un messaggio
edited_post: Ha modificato un post
banned: Ti ha sospeso
removed: Rimosso da un moderatore
deleted: Cancellato dall'autore
mentioned_you: Ti ha menzionato
comment: Commento
post: Post
ban_expired: Sospensione scaduta
purge: Rimuovi
send_message: Invia messaggio
message: Messaggio
infinite_scroll: Scroll infinito
sticky_navbar: Barra di navigazione a scomparsa
subject_reported: Il contenuto è stato segnalato.
sidebar_position: Posizione della barra laterale
left: Sinistra
right: Destra
federation: Federazione
on: On
off: Spento
instances: Istanze
upload_file: Carica file
from_url: Da url
magazine_panel: Pannello della rivista
reject: Rifiuta
ban: Sospendi
filters: Filtri
approved: Approvato
add_moderator: Aggiungi moderatore
add_badge: Aggiungi distintivo
bans: Sospensioni
created: Creato
expires: Scade
perm: Permanente
expired_at: Scaduto il
add_ban: Aggiungi sospensione
trash: Cestino
icon: Icona
done: Fatto
pin: Fissa
change: Modifica
change_magazine: Modifica rivista
change_language: Modifica lingua
pinned: Fissato
preview: Anteprima
article: Thread
reputation: Reputazione
note: Nota
users: Utenti
content: Contenuto
week: Settimana
weeks: Settimane
month: Mese
months: Mesi
year: Anno
federated: Federato
local: Locale
dashboard: Pannello di controllo
contact_email: Email di contatto
meta: Meta
instance: Istanza
pages: Pagine
FAQ: FAQ
federation_enabled: Federazione abilitata
registrations_enabled: Registrazioni abilitate
registration_disabled: Registrazioni disabilitate
restore: Ripristina
firstname: Nome
send: Invia
active_users: Utenti attivi
related_entries: Thread correlati
delete_account: Elimina account
purge_account: Rimuovi account
ban_account: Sospendi account
random_entries: Thread casuali
sidebar: Barra laterale
unban_account: Rimuovi sospensione all'account
related_magazines: Riviste correlate
random_magazines: Riviste casuali
auto_preview: Anteprima automatica media
dynamic_lists: Elenco dinamico
banned_instances: Istanze sospese
kbin_intro_title: Esplora il Fediverso
kbin_promo_title: Crea la tua istanza
captcha_enabled: Captcha abilitato
header_logo: Logo di intestazione
return: Indietro
added_new_reply: Ha aggiunto una nuova risposta
show_top_bar: Mostra la barra superiore
status: Stato
approve: Approva
rejected: Respinto
unpin: Non fissare
writing: Scrivere
admin_panel: Pannello di amministrazione
mercure_enabled: Mercure abilitato
report_issue: Segnala un problema
tokyo_night: Tokyo Night
boost: Boost
preferred_languages: Filtra le lingue dei thread e dei post
sort_by: Ordina per
filter_by_subscription: Filtra per abbonamento
filter_by_federation: Filtra per stato della federazione
created_since: Creato dal
subscribers_count: '{0}Abbonati|{1}Abbonato|]1,Inf[ Abbonati'
marked_for_deletion: Contrassegnato per la cancellazione
marked_for_deletion_at: Contrassegnato per la cancellazione in data %date%
================================================
FILE: translations/messages.ja.yaml
================================================
type.link: リンク
type.article: スレッド
type.photo: 画像
type.video: 動画
type.smart_contract: スマート契約
type.magazine: マガジン
thread: スレッド
threads: スレッド
microblog: ミニブログ
people: ユーザー
events: イベント
magazine: マガジン
magazines: マガジン
search: 検索
add: 追加
select_channel: チャンネルを選択
login: ログイン
top: トップ
hot: 人気
active: 注目
newest: 新着
oldest: 古い順
commented: コメントの多い順
change_view: 表示を変更
filter_by_time: 時間でフィルター
filter_by_type: 投稿でフィルター
favourites: お気に入り
favourite: お気に入り
more: さらに
avatar: アバター
added: 追加されました
up_votes: ブースト数
down_votes: 「良くない」数
no_comments: コメントなし
created_at: 作成された
owner: オーナー
subscribers: 購読者
online: オンライン
comments: コメント
posts: 投稿
replies: 返信
moderators: モデレーター
mod_log: モデレーターの記録
add_comment: コメントする
add_post: 投稿する
add_media: メディアを添付
markdown_howto: エディターの使い方は?
enter_your_post: 投稿を入力してください
activity: アクティビティ
cover: プロフィールのバナー
related_posts: 関連投稿
random_posts: 最近のコメント
federated_user_info: このプロフィールは連合サーバからのもので、不完全な可能性があります。
go_to_original_instance: 連合元インスタンスで表示
empty: 投稿なし
subscribe: 購読する
unsubscribe: 購読を解除
follow: フォロー
reply: 返信
login_or_email: ユーザー名またはメールアドレス
password: パスワード
dont_have_account: アカウントを持っていませんか?
you_cant_login: パスワードを忘れましたか?
already_have_account: すでにアカウントを持っていますか?
register: 新規登録する
show_more: もっと見る
to: マガジン
in: 中に
username: ユーザー名
email: メールアドレス
terms: 利用規約
privacy_policy: プライバシーポリシー
about_instance: このインスタンスについて
all_magazines: すべてのマガジン
stats: 統計
add_new_article: 新しいスレッドを追加
add_new_link: 新しいリンクを追加
add_new_post: 新しい投稿を追加
add_new_video: 新しい動画を追加
contact: お問い合わせ
faq: FAQ
rss: RSS
change_theme: テーマ変更
useful: 便利リンク
help: ヘルプ
reset_check_email_desc2: メールを受信しなかったら、迷惑メールのフォルダを確認してください。
try_again: もう一度
up_vote: ブースト
down_vote: 「よくない」
email_confirm_header: こんにちは!メールアドレスを検証してください。
email_confirm_content: 準備はいいですか? あなたのMbinアカウントを有効にするには下のリンクをクリックしてください。
email_verify: メールアドレスの検証
email_confirm_title: メールアドレスを検証してください。
select_magazine: マガジンを選択
add_new: 追加する
url: URL
title: タイトル
body: 本文
tags: タグ
badges: バッジ
is_adult: R-18 / NSFW
eng: 英語
oc: オリジナルコンテンツ
image: 画像
name: 名前
description: 詳細
rules: ルール
domain: ドメイン
followers: フォロワー
following: フォロー中
subscriptions: 購読
overview: 概要
cards: カード
columns: 列
user: ユーザー
joined: 登録日
people_local: ローカル
people_federated: 連合
reputation_points: 評判ポイント
related_tags: 関連タグ
go_to_content: コンテンツへ
go_to_filters: フィルターへ
go_to_search: 検索へ
subscribed: 購読中
all: すべて
logout: ログアウト
links: リンク
compact_view: コンパクト表示
chat_view: チャット表示
tree_view: ツリー表示
cards_view: カード表示
3h: 3時間
6h: 6時間
12h: 12時間
1d: 1日
1w: 1週間
1m: 1ヶ月
1y: 1年
articles: スレッド
photos: 画像
videos: 動画
report: 通報
share: 共有
copy_url_to_fediverse: オリジナルURLをコピー
share_on_fediverse: フェディバースへシェア
edit: 編集
are_you_sure: よろしいですか?
moderate: モデレートする
reason: 理由
delete: 削除する
edit_post: 投稿を編集する
settings: 設定
profile: プロフィール
blocked: ブロック
reports: 通報
notifications: 通知
messages: メッセージ
appearance: 外観
homepage: ホーム
comments_count: '{0} コメント | {1} コメント | ]1,Inf[ コメント'
agree_terms: '%terms_link_start% 利用規約 %terms_link_end% と %policy_link_start% プライバシーポリシー
%policy_link_end% を同意する'
email_confirm_expire: リンクの有効期限は1時間です。
enter_your_comment: コメントを入力してください
fediverse: フェディバース
create_new_magazine: 新しいマガジンを作成
federated_magazine_info: このマガジンは連合サーバからのもので、不完全な可能性があります。
add_new_photo: 新しい画像を追加
image_alt: 画像の代替テキスト
moderated: モデレートしているマガジン
check_email: メールを確認してください
unfollow: フォローを解除
reset_check_email_desc:
あなたのアカウントに紐づくメールアドレスが存在する場合、パスワードをリセットするためのメールが送信されます。このリンクの有効期限は %expire%
までです。
remember_me: ログイン情報を保存
reset_password: パスワードをリセットする
repeat_password: パスワードを再入力
classic_view: クラシック表示
table_view: テーブル表示
copy_url: MbinのURLをコピー
edit_comment: 上書き保存
general: 全般
hide_adult: NSFWのコンテンツを非表示にする
featured_magazines: おすすめのマガジン
privacy: プライバシー
show_profile_subscriptions: 購読しているマガジンを表示
show_profile_followings: フォローしているユーザーを表示する
notify_on_new_post_reply: 自分の投稿のリプライを通知する
notify_on_new_entry_reply: 自分のスレッドのコメントを通知する
notify_on_new_entry_comment_reply: 自分のスレッドのコメントのリプライを通知する
notify_on_new_post_comment_reply: 自分の投稿のコメントのリプライを通知する
notify_on_new_entry: 購読しているマガジンの新スレッドを通知する
notify_on_new_posts: 購読しているマガジンの新規投稿を通知する
save: 保存
about: Mbinについて
old_email: 現在のメールアドレス
new_email: 新しいメールアドレス
new_email_repeat: もう一度新しいメールを入力
current_password: 現在のパスワード
new_password: 新しいパスワード
new_password_repeat: もう一度新しいパスワードを入力
change_email: メールアドレスの変更
change_password: パスワードの変更
expand: もっと表示する
collapse: 小さく表示する
domains: ドメイン
error: エラー
votes: 投票
theme: テーマ
dark: ダーク
light: ライト
solarized_light: Solarizedライト
solarized_dark: Solarizedダーク
size: サイズ
boosts: ブースト
show_users_avatars: ユーザーのアバターを表示する
yes: はい
no: いいえ
show_magazines_icons: マガジンのアイコンを表示
show_thumbnails: サムネイルを表示する
removed_thread_by: はこのアカウントが作成したスレッドを削除しました:
removed_comment_by: はこのアカウントが投稿したコメントを削除しました:
restored_comment_by: はこのアカウントが投稿したコメントを復旧しました:
restored_post_by: はこのアカウントが作成した投稿を復旧しました:
he_banned: バンする
he_unbanned: バン解除
read_all: 全部既読にする
show_all: 全部表示する
flash_thread_new_success: スレッドが作成されました。他のユーザーがスレッドを見られます。
flash_thread_edit_success: スレッド編集ができました。
flash_thread_delete_success: スレッドを削除しました。
flash_thread_pin_success: スレッドを固定しました。
flash_magazine_edit_success: マガジンを編集しました。
too_many_requests: 制限を超えました。時間が経ってからもう一度試してください。
set_magazines_bar: マガジンバー
set_magazines_bar_desc: マガジン名はカンマで区切って入力してください
set_magazines_bar_empty_desc: 空欄にするとアクティブなマガジンが表示されます。
font_size: 文字サイズ
rounded_edges: 丸角
flash_register_success:
ようこそ!アカウント登録ができました。最後の手順として、メールアドレスにアカウント検証リンクを送信しました。メールボックスを確認して下さい。
restored_thread_by: はこのアカウントが作成したスレッドを復旧しました:
removed_post_by: はこのアカウントが作成した投稿を削除しました:
flash_thread_unpin_success: スレッドの固定を外しました。
flash_magazine_new_success: マガジンを作成しました。新しいコンテンツを追加したりマガジンパネルから検索することができます。
added_new_thread: 新しいスレッドを追加しました
edited_thread: スレッドを編集しました
added_new_comment: 新しいコメントを追加しました
edited_comment: コメントを編集しました
replied_to_your_comment: あなたのコメントにリプライしました
added_new_post: 新しい投稿を追加しました
edited_post: 投稿を編集しました
added_new_reply: 新しいリプライを追加しました
wrote_message: メッセージを書きました
banned: あなたをバンしました
removed: モデレーターに削除されました
mod_log_alert: モデレーターの記録ではモデレーターに削除された過激な内容が表示される可能性があります。大変ご注意ください。
mod_remove_your_thread: モデレーターにあなたのスレッドが削除されました
mod_deleted_your_comment: モデレーターにあなたのコメントが削除されました
mod_remove_your_post: モデレーターにあなたの投稿が削除されました
deleted: 作成者に削除されました
mentioned_you: はあなた宛てに言った
post: 投稿する
comment: コメントする
ban_expired: バンの期限切れました
purge: 消去する
send_message: ダイレクトメッセージを送る
message: メッセージ
infinite_scroll: 無限スクロール
show_top_bar: トップバーを表示する
sticky_navbar: ナビゲーションバーを固定する
subject_reported: コンテンツが通報されました。
sidebar_position: サイドバーの位置
left: 左
right: 右
federation: 連合
status: ステータス
on: 有効
off: 無効
instances: インスタンス
upload_file: ファイルをアップロード
from_url: URLからアップロード
magazine_panel: マガジンパネル
reject: 拒否する
approve: 許可する
ban: バンする
filters: フィルター
approved: 許可された
rejected: 拒否された
add_moderator: モデレーターを追加する
add_badge: バッジを追加する
bans: バンの回数
created: 作成した
expires: 有効期限
perm: 無期限
expired_at: 期限切れ:
add_ban: バンを追加する
trash: ゴミ箱に入れる
icon: アイコン
done: 完了
pin: 固定する
unpin: 固定を外す
change_magazine: マガジンを変更
change_language: 言語を変更
change: 変更する
pinned: 固定された
preview: プレビュー表示
article: スレッド
reputation: 評判度
note: ノート
writing: 著作
users: ユーザー
content: コンテンツ
weeks: 週間
week: 1週間
month: 1ヶ月間
months: 月間
year: 1年間
local: ローカル
admin_panel: 管理パネル
dashboard: ダッシュボード
contact_email: 連絡先のメールアドレス
meta: メタ情報
instance: インスタンス
pages: ページ
FAQ: FAQ
registrations_enabled: 新規登録可能
federation_enabled: 連合を有効
registration_disabled: 新規登録不可
restore: 復旧する
add_mentions_posts: タグを投稿に自動的につける
Password is invalid: パスワードが違います。
Your account is not active: アカウントは無効です。
firstname: 名前
send: 送信する
active_users: アクティブなユーザー
random_entries: 最近のスレッド
related_entries: 関連のスレッド
delete_account: アカウントを削除する
purge_account: アカウントを除去する
ban_account: アカウントをバンする
related_magazines: 関連するマガジン
random_magazines: ランダムなマガジン
sidebar: サイドバー
auto_preview: メディア自動プレビュー
dynamic_lists: ダイナミックリスト
banned_instances: バンされたインスタンス
kbin_intro_title: フェディバースに参加しよう
kbin_promo_title: 自分のインスタンスを作る
kbin_promo_desc: '%link_start% リポジトリをクローンして %link_end% フェディバースを広げましょう'
captcha_enabled: Captchaは有効
header_logo: ヘッダーロゴ
return: 戻る
federated: 連合している
type_search_term: 検索入力
add_mentions_entries: タグを自動的に追加する
Your account has been banned: アカウントは停止されています。
unban_account: アカウントのバンを解除する
magazine_panel_tags_info: 連合するサーバーからマガジンへ特定のタグの投稿を自動投稿する場合は、タグを入力します
kbin_intro_desc: はフェディバースネットワーク内で動作する、コンテンツの集約とミニブログのための分散型プラットフォームです。
browsing_one_thread: 現在ディスカッション内の一つのスレッドだけを見ています!すべてのコメントは投稿ページから見られます。
boost: ブースト
mercure_enabled: Mercureが有効
report_issue: バグを報告
tokyo_night: 東京ナイト
oauth2.grant.post.edit: 既存ポストを編集 。
oauth2.grant.user.message.all: メッセージを読んで、 他ユーザーにメッセージを飛ばす。
oauth2.grant.moderate.magazine_admin.create: 新しいマガジンを作る。
filter.adult.hide: R18/NSFWを見せない
toolbar.bold: ボールド
toolbar.header: ヘッダー
oauth2.grant.user.message.read: メッセージを読む。
filter.fields.only_names: 名前だけ
oauth2.grant.entry.create: 新しいスレッドを作る。
filter.adult.show: R18/NSFWを見せる
oauth.consent.allow: 許す
filter.adult.only: R18/NSFWだけ
filter.fields.names_and_descriptions: 名前と詳細
oauth2.grant.user.profile.all: プロフィールを読んで、 エディットする。
oauth2.grant.entry_comment.create: スレッドに新しいコメントを作る。
oauth.consent.deny: 打ち消す
oauth2.grant.entry_comment.delete: スレッドの既存コメントを消す。
subject_reported_exists: このコンテンツはもう報告しました。
oauth2.grant.entry.edit: 既存スレッドを編集する。
oauth2.grant.entry_comment.edit: スレッドの既存コメントを編集する。
oauth2.grant.user.profile.read: プロフィールを読む。
toolbar.link: リンク
moderation.report.ban_user_title: ユーザーを禁止します
oauth2.grant.entry.delete: 既存スレッドを消す。
toolbar.code: コード
oauth2.grant.user.message.create: 他ユーザーにメッセージを飛ばす。
update_comment: コメントをアップデート
oauth2.grant.post_comment.create: ポストに新しいコメントを作る。
oauth2.grant.post.delete: 既存ポストを消す。
oauth2.grant.post.create: 新しいポストを作る。
toolbar.image: 映像
toolbar.italic: 斜体
oauth2.grant.user.profile.edit: プロフィールをエディット。
moderation.report.approve_report_title: 通知を許す
moderation.report.reject_report_title: 通知を断る
errors.server429.title: 429 Too Many Requests
local_and_federated: ローカルと連合
toolbar.ordered_list: 順序付きリスト
custom_css: カスタムCSS
toolbar.quote: 引用
toolbar.unordered_list: 順序不同リスト
errors.server404.title: 404 Not found
resend_account_activation_email: もう一度アカウントアクティブ化メールを飛ばす
errors.server500.title: 500 Internal Server Error
toolbar.mention: メンション
oauth2.grant.write.general: スレッド、投稿もしくはコメントを作るか、編集する。
single_settings: 単一
resend_account_activation_email_question: 非活動アカウント?
your_account_has_been_banned: あなたのアカウントはバンされました
oauth2.grant.delete.general: スレッド、投稿もしくはコメントを消す。
oauth2.grant.user.notification.delete: 通知を消去する。
email.delete.title: ユーザーのアカウント削除を要望する
comment_reply_position: コメントのレスの位置
kbin_bot: Mbinのボット
federation_page_enabled: 連合ページは有効化されました
show_avatars_on_comments: コメントでアバターを見せる
tag: タグ
remove_media: メディアを削除
edit_entry: スレッドを編集
show_related_magazines: ランダムなマガジンを表示
default_theme: デフォルトのテーマ
solarized_auto: Solarized(自動検出)
default_theme_auto: ライト/ダーク(自動検出)
menu: メニュー
federation_page_allowed_description: 連合している既知のインスタンス
federation_page_disallowed_description: 連合していないインスタンス
toolbar.spoiler: スポイラー
errors.server403.title: 403 Forbidden
subscription_header: 購読中のマガジン
federation_page_dead_title: 消息不明インスタンス
toolbar.strikethrough: 取り消し線
federation_page_dead_description: 10回のアクティビティが連続で配送に失敗したか、一週間以上配送に成功していないインスタンス
federated_search_only_loggedin: ログインしていない場合は連合検索が制限されます
ignore_magazines_custom_css: マガジンのカスタムCSSを無視する
show_related_entries: ランダムなスレッドを表示
show_related_posts: ランダムな投稿を表示
show_active_users: アクティブなユーザーを表示
================================================
FILE: translations/messages.nb_NO.yaml
================================================
{}
================================================
FILE: translations/messages.nl.yaml
================================================
show_profile_subscriptions: Tijdschriftabonnementen tonen
notify_on_new_entry_comment_reply: Stel me op de hoogte van reacties op alle
gesprekken
notify_on_new_posts: Stel me op de hoogte van nieuwe berichten in een
tijdschrift waarop ik geabonneerd ben
solarized_light: Gepolariseerd Licht
show_magazines_icons: Tijdschriftpictogrammen tonen
removed_post_by: heeft een bericht verwijderd van
flash_register_success: Welkom aan boord, je account is nu geregistreerd. Nog
één stap! - Check je inbox voor een activatielink die je account tot leven zal
brengen.
type.article: Gesprek
type.photo: Foto
type.video: Video
type.smart_contract: Slim contract
type.magazine: Tijdschrift
thread: Gesprek
microblog: Microblog
people: Mensen
events: Evenementen
magazine: Tijdschrift
magazines: Tijdschriften
search: Zoeken
add: Toevoegen
login: Inloggen
top: Top
hot: Populair
active: Actief
newest: Nieuwste
oldest: Oudste
commented: Gereageerd
change_view: Andere weergave
filter_by_time: Filteren op tijdstip
filter_by_type: Filteren op type
favourites: Omhoog stemmen
favourite: Favoriet
more: Meer
avatar: Profielfoto
added: Toegevoegd
no_comments: Geen reacties
created_at: Aangemaakt
owner: Eigenaar
subscribers: Abonnees
online: Online
comments: Reacties
posts: Berichten
moderators: Moderators
mod_log: Moderatielogboek
add_comment: Reactie plaatsen
add_post: Bericht plaatsen
add_media: Media plaatsen
enter_your_comment: Voer een reactie in
enter_your_post: Voer een bericht in
activity: Activiteit
cover: Omslag
related_posts: Verwante berichten
random_posts: Willekeurige berichten
federated_user_info: Dit profiel is van een gefedereerde server en is mogelijk
onvolledig.
go_to_original_instance: Bekijk op externe instantie
empty: Leeg
subscribe: Abonneren
down_votes: Omlaagstemmen
up_votes: Omhoogstemmen
follow: Volgen
unfollow: Ontvolgen
login_or_email: Gebruikersnaam of e-mailadres
password: Wachtwoord
remember_me: Ingelogd blijven
dont_have_account: Heb je nog geen account?
already_have_account: Heb je al een account?
register: Registreren
reset_password: Wachtwoord herstellen
show_more: Meer informatie
to: aan
in: in
username: Gebruikersnaam
email: E-mailadres
terms: Algemene voorwaarden
privacy_policy: Privacybeleid
about_instance: Over
all_magazines: Alle tijdschriften
stats: Statistieken
fediverse: Fediverse
create_new_magazine: Nieuw tijdschrift maken
add_new_article: Artikel toevoegen
add_new_link: Link toevoegen
add_new_photo: Foto toevoegen
add_new_post: Bericht toevoegen
add_new_video: Video toevoegen
contact: Contact
faq: Veelgestelde vragen
rss: RSS
change_theme: Thema wijzigen
useful: Nuttig
help: Hulp
check_email: Controleer je postvak in
reset_check_email_desc2: Wanneer je geen e-mail hebt ontvangen, controleer dan
je spammap.
try_again: Probeer opnieuw
up_vote: Boost
down_vote: Stem omlaag
email_confirm_header: Hallo! Bevestig je e-mailadres.
email_confirm_content: 'Klaar om je Mbin account te activeren? Klik dan op onderstaande
link:'
email_verify: E-mailadres bevestigen
email_confirm_expire: 'Let op: de link is 1 uur geldig.'
email_confirm_title: Bevestig je e-mailadres.
select_magazine: Selecteer een tijdschrift
add_new: Toevoegen
url: Url
title: Titel
body: Inhoud
badges: Emblemen
eng: ENG
oc: OI
image: Afbeelding
name: Naam
description: Omschrijving
rules: Regels
domain: Domeinnaam
followers: Aantal volgers
following: Aantal gevolgden
subscriptions: Aantal abonnementen
overview: Overzicht
cards: Kaarten
columns: Kolommen
user: Gebruiker
joined: Lid
moderated: Met moderatie
people_local: Lokaal
people_federated: Gefedereerd
reputation_points: Aantal reputatiepunten
related_tags: Verwante labels
go_to_content: Ga naar inhoud
go_to_filters: Ga naar filters
subscribed: Geabonneerd
all: Alles
logout: Uitloggen
classic_view: Klassieke weergave
compact_view: Compacte weergave
chat_view: Gespreksweergave
tree_view: Boomweergave
cards_view: Kaartenweergave
3h: 3u
6h: 6u
12h: 12u
1d: 1d
1w: 1w
1m: 1m
1y: 1j
links: Links
articles: Artikelen
photos: Foto's
report: Melden
share: Delen
copy_url: Kopieer Mbin URL
copy_url_to_fediverse: Kopieer origineel URL
share_on_fediverse: Delen op fediverse
edit: Bewerken
moderate: Modereren
reason: Reden
delete: Verwijderen
edit_post: Bericht bewerken
show_profile_followings: Gevolgde gebruikers tonen
notify_on_new_entry_reply: Stel me op de hoogte van reacties in gesprekken die
ik heb geschreven
notify_on_new_post_reply: Stel me op de hoogte van reacties op berichten die ik
heb geschreven
notify_on_new_post_comment_reply: Stel me op de hoogte van reacties op alle
berichten
notify_on_new_entry: Stel me op de hoogte van nieuwe gesprekken (links of
artikelen) in elk tijdschrift waarop ik op ben geabonneerd
save: Opslaan
about: Over
old_email: Huidig e-mailadres
new_email: Nieuw e-mailadres
new_email_repeat: E-mailadres bevestigen
current_password: Huidig wachtwoord
new_password: Nieuw wachtwoord
settings: Instellingen
general: Algemeen
profile: Profiel
blocked: Geblokkeerd
new_password_repeat: Nieuw wachtwoord bevestigen
notifications: Meldingen
messages: Berichten
appearance: Uiterlijk
change_email: E-mailadres wijzigen
homepage: Startpagina
hide_adult: Inhoud voor volwassenen verbergen
featured_magazines: Uitgelichte tijdschriften
privacy: Privacy
change_password: Wachtwoord wijzigen
expand: Uitklappen
collapse: Inklappen
domains: Domeinnamen
error: Foutmelding
votes: Stemmen
theme: Thema
dark: Donker
light: Licht
font_size: Tekstgrootte
solarized_dark: Gepolariseerd Donker
size: Grootte
show_users_avatars: Profielfoto's tonen
yes: Ja
no: Nee
show_thumbnails: Miniaturen tonen
rounded_edges: Afgeronde hoeken
removed_thread_by: heeft een gesprek verwijderd van
restored_thread_by: heeft een gesprek hersteld van
removed_comment_by: heeft een reactie verwijderd van
restored_comment_by: heeft een reactie hersteld van
restored_post_by: heeft een bericht hersteld van
he_banned: Verbannen
he_unbanned: Ongeblokkeerd
read_all: Alles als gelezen markeren
show_all: Alles tonen
flash_thread_new_success: Het gesprek is succesvol aangemaakt en zichtbaar voor
andere gebruikers.
flash_thread_edit_success: Het gesprek is succesvol bewerkt.
flash_thread_delete_success: Het gesprek is succesvol verwijderd.
flash_thread_pin_success: Het gesprek is succesvol vastgemaakt.
flash_thread_unpin_success: Het gesprek is succesvol losgemaakt.
type.link: Link
threads: Gesprekken
select_channel: Kies een kanaal
comments_count: '{0}Reacties|{1}Reactie|]1,Inf[ Reacties'
replies: Antwoorden
markdown_howto: Hoe werkt het bewerkveld?
federated_magazine_info: Dit tijdschrift is van een gefedereerde server en is
mogelijk onvolledig.
unsubscribe: Deabonneren
reply: Beantwoorden
you_cant_login: Wachtwoord vergeten?
repeat_password: Wachtwoord herhalen
agree_terms: Ik ga akkoord met de %terms_link_start%algemene
voorwaarden%terms_link_end% and %policy_link_start%Privacy
Policy%policy_link_end%
reset_check_email_desc: 'Als er een account bestaat dat overeenkomt met je e-mailadres,
dan ontvang je op korte termijn een email met een link om je wachtwoord opnieuw
in te stellen. Deze link vervalt over %expire%.'
boosts: Omhoogstemmen
tags: Labels
is_adult: 18+/NSFW
image_alt: Afbeeldingslabel
go_to_search: Ga naar zoeken
table_view: Tabelweergave
videos: Video's
are_you_sure: Weet je het zeker?
edit_comment: Wijzigingen opslaan
reports: Gemelde berichten
banned_instances: Verbannen instanties
deleted: Verwijderd door de auteur
sticky_navbar: Bovenbalk meeschuiven
mod_log_alert: WAARSCHUWING - Het moderatielogboek kan onaangename of
schrijnende inhoud verwijderd door moderators bevatten. Wees voorzichtig.
upload_file: Bestand uploaden
created: Toegekend op
add_ban: Verbanning toekennen
change_language: Taal wijzigen
admin_panel: Beheerderspaneel
type_search_term: Voer een zoekopdracht in
add_mentions_entries: Vermeldingslabels toekennen aan gesprekken
related_entries: Verwante gesprekken
flash_magazine_new_success: Het tijdschrift is succesvol aangemaakt. Je kunt
inhoud toevoegen of het tijdschrift bekijken in het beheerpaneel.
flash_magazine_edit_success: Het tijdschrift is succesvol bewerkt.
too_many_requests: Het limiet is bereikt - probeer het later opnieuw.
set_magazines_bar: Tijdschriftenbalk
set_magazines_bar_desc: Voeg tijdschriftnamen toe achter de komma
set_magazines_bar_empty_desc: Als het veld blanco is, dan worden actieve
tijdschriften op de balk getoond.
mod_remove_your_thread: Een moderator heeft je gesprek verwijderd
added_new_thread: Gesprek toegevoegd
edited_thread: Gesprek bewerkt
added_new_comment: Plaats een nieuwe reactie
edited_comment: Reactie bewerkt
replied_to_your_comment: Heeft je reactie beantwoord
mod_deleted_your_comment: Een moderator heeft je reactie verwijderd
added_new_post: Bericht geplaatst
edited_post: Bericht bewerkt
mod_remove_your_post: Een moderator heeft je bericht verwijderd
added_new_reply: Nieuwe reactie is geplaatst
wrote_message: Bericht geschreven
banned: Heeft je verbannen
removed: Verwijderd door een moderator
mentioned_you: Heeft je vermeld
comment: Reactie
post: Bericht
purge: Vernietigen
send_message: Direct bericht versturen
message: Bericht
infinite_scroll: Oneindig scrollen
show_top_bar: Bovenbalk tonen
subject_reported: Er is melding gemaakt van de inhoud.
sidebar_position: Zijbalklocatie
left: Linkerkant
right: Rechterkant
federation: Verspreiding
status: Status
on: Aan
off: Uit
instances: Instanties
from_url: Van url
magazine_panel: Tijdschriftpaneel
reject: Afwijzen
approve: Goedkeuren
ban: Verbannen
filters: Filters
approved: Goedgekeurd
rejected: Afgewezen
add_moderator: Moderator toevoegen
add_badge: Embleem toekennen
bans: Verbanningen
expires: Vervalt op
perm: Permanent
expired_at: Vervallen op
ban_expired: De verbanperiode is afgelopen
trash: Prullenbak
icon: Pictogram
done: Klaar
pin: Vastmaken
unpin: Losmaken
change_magazine: Tijdschrift wijzigen
change: Wijzigen
pinned: Vastgemaakt
preview: Voorvertoning
article: Artikel
reputation: Reputatie
note: Opmerking
writing: Schrijven
users: Gebruikers
content: Inhoud
week: Week
weeks: Weken
month: Maand
months: Maanden
year: Jaar
federated: Verspreid
local: Lokaal
dashboard: Overzichtspaneel
contact_email: Contactadres
meta: Meta
instance: Instantie
pages: Pagina's
FAQ: Veelgestelde vragen
federation_enabled: Verspreiding is ingeschakeld
registrations_enabled: Registratie zijn ingeschakeld
registration_disabled: Registraties zijn uitgeschakeld
restore: Herstellen
add_mentions_posts: Vermeldingslabels toekennen aan berichten
Password is invalid: Het wachtwoord is ongeldig.
Your account is not active: Je account is inactief.
Your account has been banned: Je account is verbannen.
firstname: Voornaam
send: Versturen
active_users: Actieve mensen
random_entries: Willekeurige gesprekken
delete_account: Account verwijderen
purge_account: Account vernietigen
ban_account: Account verbannen
unban_account: Account toelaten
related_magazines: Verwante tijdschriften
random_magazines: Willekeurige tijdschriften
magazine_panel_tags_info: Alleen als je inhoud van het fediverse op basis van
labels in dit tijdschrift wilt tonen
sidebar: Zijbalk
auto_preview: Media automatisch voorvertonen
dynamic_lists: Dynamische lijsten
kbin_intro_title: Ontdek de Fediverse
kbin_intro_desc: is een gedecentraliseerd platform voor het verzamelen van
inhoud en microbloggen dat opereert binnen het fediversenetwerk.
kbin_promo_title: Zet je eigen instantie op
kbin_promo_desc: '%link_start%Kloon de repo%link_end% en ontwikkel het fediverse'
captcha_enabled: Captcha ingeschakeld
header_logo: Koplogo
browsing_one_thread: Je bekijkt slechts één gesprek binnen deze discussie. Je
kunt alle reacties lezen op de berichtpagina.
return: Terug
boost: Omhoogstemmen
mercure_enabled: Mercure ingeschakeld
report_issue: Rapporteer probleem
tokyo_night: Tokyo Night
preferred_languages: Filter op talen van gesprekken en berichten
infinite_scroll_help: Laad automatisch meer content zodra je de onderkant van de
pagina hebt bereikt.
sticky_navbar_help: De navigatiebalk blijft bovenaan de pagina staan wanneer u
naar beneden scrolt.
auto_preview_help: Geef de mediavoorbeelden (foto's, video's) in een groter
formaat weer onder de inhoud.
reload_to_apply: Laad de pagina opnieuw om wijzigingen toe te passen
filter.origin.label: Kies herkomst
filter.fields.label: Kies in welke velden u wilt zoeken
filter.adult.hide: NSFW verbergen
filter.adult.show: NSFW weergeven
filter.adult.only: Enkel NSFW
filter.fields.only_names: Enkel op namen
filter.fields.names_and_descriptions: Namen en beschrijvingen
filter.adult.label: Kies of u NSFW (niet veilig voor werk) wilt weergeven
local_and_federated: Lokaal en gefedereerd
kbin_bot: Mbin Agent
bot_body_content: "Welkom bij de Mbin Agent! Deze bot agent speelt een cruciale rol
bij het inschakelen van de ActivityPub-functionaliteit binnen Mbin. Het zorgt ervoor
dat Mbin kan communiceren en federeren met andere instanties in de fediverse.\n\n\
\ ActivityPub is een open standaardprotocol waarmee gedecentraliseerde sociale netwerkplatforms
met elkaar kunnen communiceren en interacteren. Het stelt gebruikers in staat om
op verschillende instanties (servers) inhoud te volgen, te gebruiken en te delen
via het gefedereerde sociale netwerk dat bekend staat als het fediverse. Het biedt
een gestandaardiseerde manier voor gebruikers om inhoud te publiceren, andere gebruikers
te volgen en deel te nemen aan sociale interacties zoals liken, delen en reageren
op threads of berichten."
password_confirm_header: Bevestig het verzoek om je wachtwoord te wijzigingen.
your_account_is_not_active: Je account is niet geactiveerd. Controleer uw e-mail
voor instructies voor accountactivatie of vraag een
nieuwe e-mail voor accountactivatie aan.
your_account_has_been_banned: Je account is verbannen
toolbar.bold: Vetgedrukt
toolbar.italic: Cursief
toolbar.header: Koptekst
toolbar.quote: Citaat
toolbar.code: Code
toolbar.link: Koppeling
toolbar.image: Afbeelding
toolbar.strikethrough: Doorhalen
toolbar.unordered_list: Ongeordende lijst
toolbar.ordered_list: Geordende lijst
toolbar.mention: Vermelding
federation_page_disallowed_description: Instanties waarmee we niet mee
gefedereerd zijn
federated_search_only_loggedin: Federatieve zoekopdracht is beperkt indien niet
ingelogd
oauth2.grant.user.notification.read: Lees uw meldingen, inclusief
berichtmeldingen.
oauth2.grant.user.oauth_clients.edit: Bewerk de machtigingen die u aan andere
OAuth2-applicaties hebt verleend.
oauth2.grant.user.follow: Volg of ontvolg gebruikers en lees een lijst met
gebruikers die u volgt.
more_from_domain: Meer van domein
oauth2.grant.user.block: Blokkeer of deblokkeer gebruikers en lees een lijst met
gebruikers die u blokkeert.
resend_account_activation_email_error: Er is een probleem opgetreden bij het
indienen van dit verzoek. Mogelijk is er geen account aan dat e-mailadres
gekoppeld of is het misschien al geactiveerd.
resend_account_activation_email_success: Als er een account bestaat dat aan dat
e-mailadres is gekoppeld, sturen we een nieuwe activatie-e-mail.
oauth2.grant.moderate.all: Voer elke moderatieactie uit waarvoor u toestemming
heeft om deze uit te voeren in uw gemodereerde tijdschriften.
resend_account_activation_email_description: Voer het e-mailadres in dat aan uw
account is gekoppeld. Wij sturen u nogmaals een activatie-e-mail.
oauth2.grant.moderate.entry.all: Beheer discussies in uw gemodereerde
tijdschriften.
oauth.consent.grant_permissions: Verleen machtigingen
oauth2.grant.moderate.entry.pin: Zet discussies vast bovenin uw gemodereerde
tijdschriften.
oauth.consent.app_requesting_permissions: wil namens u de volgende handelingen
uitvoeren
oauth2.grant.moderate.entry.set_adult: Markeer discussies als NSFW in uw
gemodereerde tijdschriften.
oauth.consent.app_has_permissions: kan de volgende acties al uitvoeren
oauth2.grant.moderate.entry.trash: Verwijder of herstel discussies in uw
gemodereerde tijdschriften.
oauth.consent.to_allow_access: Om deze toegang toe te staan, klikt u hieronder
op de knop 'Toestaan'
oauth2.grant.moderate.entry_comment.change_language: Verander de taal van
reacties in discussies in uw gemodereerde tijdschriften.
oauth.consent.allow: Toestaan
oauth.consent.deny: Weigeren
oauth2.grant.moderate.entry_comment.set_adult: Markeer reacties in discussies
als NSFW in uw gemodereerde tijdschriften.
oauth.client_identifier.invalid: Ongeldige OAuth-client-ID!
oauth.client_not_granted_message_read_permission: Deze app heeft geen
toestemming gekregen om jouw berichten te lezen.
restrict_oauth_clients: Beperk het maken van OAuth2-clients tot Beheerders
oauth2.grant.domain.all: Abonneer u op domeinen of geblokkeerde domeinen en
bekijk de domeinen waarop u zich abonneert of blokkeert bent.
oauth2.grant.domain.block: Blokkeer of deblokkeer domeinen en bekijk de domeinen
die u hebt geblokkeerd.
oauth2.grant.moderate.post.change_language: Verander de taal van berichten in uw
gemodereerde tijdschriften.
oauth2.grant.moderate.post.set_adult: Markeer berichten als NSFW in uw
gemodereerde tijdschriften.
oauth2.grant.entry.all: Maak, bewerk of verwijder uw discussies en stem, promoot
of rapporteer elke discussie.
oauth2.grant.moderate.post.trash: Verwijder of herstel berichten in uw
gemodereerde tijdschriften.
oauth2.grant.entry.vote: Stem een discussie omhoog, omlaag of boost.
oauth2.grant.moderate.post_comment.all: Beheer reacties op on berichten in uw
gemodereerde tijdschriften.
oauth2.grant.moderate.post_comment.change_language: Wijzig de taal van reacties
op berichten in uw gemodereerde tijdschriften.
oauth2.grant.entry.report: Rapporteer elke discussie.
oauth2.grant.moderate.post_comment.set_adult: Markeer reacties op berichten als
NSFW in uw gemodereerde tijdschriften.
oauth2.grant.entry_comment.edit: Bewerk uw bestaande opmerkingen in discussies.
oauth2.grant.moderate.post_comment.trash: Verwijder of herstel reacties op
berichten in uw gemodereerde tijdschriften.
oauth2.grant.entry_comment.report: Rapporteer elke opmerking in een discussie.
oauth2.grant.moderate.magazine.ban.all: Beheer verbannen gebruikers in uw
gemodereerde tijdschriften.
oauth2.grant.magazine.all: Abonneer u op tijdschriften of blokkeer ze en bekijk
de tijdschriften waarop u geabonneerd of geblokkeerd bent.
oauth2.grant.magazine.subscribe: Schrijf je in of uit op tijdschriften en bekijk
de tijdschriften waarop je geabonneerd bent.
oauth2.grant.post.all: Maak, bewerk of verwijder uw microblogs en stem, promoot
of rapporteer microblogs.
oauth2.grant.moderate.magazine.ban.delete: Hef de ban van gebruikers op in uw
gemodereerde tijdschriften.
oauth2.grant.post_comment.delete: Verwijder uw bestaande reacties op berichten.
oauth2.grant.user.all: Lees en bewerk uw profiel, berichten of meldingen; Lees-
en bewerk machtigingen die u aan andere apps hebt verleend; andere gebruikers
volgen of blokkeren; bekijk lijsten met gebruikers die u volgt of blokkeert.
oauth2.grant.moderate.magazine_admin.update: Bewerk de regels, beschrijving,
NSFW-status of het pictogram van uw eigen tijdschriften.
oauth2.grant.admin.all: Voer administratieve actie uit op uw instantie.
oauth2.grant.admin.entry.purge: Verwijder alle discussies volledig uit uw
instantie.
last_active: Laatst actief
oauth2.grant.moderate.magazine.list: Lees een lijst van uw gemodereerde
tijdschriften.
oauth2.grant.moderate.magazine.reports.all: Beheer rapporten in uw gemodereerde
tijdschriften.
oauth2.grant.moderate.magazine.reports.read: Lees rapporten in uw gemodereerde
tijdschriften.
oauth2.grant.moderate.magazine.reports.action: Accepteer of wijs rapporten af in
uw gemodereerde tijdschriften.
oauth2.grant.moderate.magazine.trash.read: Bekijk weggegooide inhoud in uw
gemodereerde tijdschriften.
oauth2.grant.moderate.magazine_admin.all: Maak, bewerk of verwijder uw eigen
tijdschriften.
oauth2.grant.moderate.magazine_admin.create: Nieuwe tijdschriften maken.
oauth2.grant.moderate.magazine_admin.delete: Verwijder alle tijdschriften die u
bezit.
oauth2.grant.moderate.magazine_admin.edit_theme: Bewerk de aangepaste CSS van al
uw tijdschriften.
oauth2.grant.moderate.magazine_admin.moderators: Voeg of verwijder moderators op
een van uw tijdschriften.
oauth2.grant.moderate.magazine_admin.badges: Maak of verwijder badges uit uw
eigen tijdschriften.
oauth2.grant.moderate.magazine_admin.tags: Maak of verwijder tags uit uw eigen
tijdschriften.
oauth2.grant.moderate.magazine_admin.stats: Bekijk de inhoud, stem en bekijk
statistieken van uw eigen tijdschriften.
errors.server500.title: 500 Interne Server Foutmelding
errors.server500.description: Sorry! Er is iets fout gegaan aan onze kant. Als u
deze fout blijft zien, kunt u contact opnemen met de eigenaar van deze
instantie. Als deze instantie helemaal niet werkt, bekijk dan in de tussentijd
%link_start%andere Mbin-instanties%link_end% totdat het probleem is opgelost.
errors.server429.title: 429 Te Veel Verzoeken
errors.server404.title: 404 Niet Gevonden
errors.server403.title: 403 Verboden
email_confirm_button_text: Bevestig uw verzoek tot wachtwoordwijziging
email_confirm_link_help: Als alternatief kunt u het volgende kopiëren en plakken
in uw browser
email.delete.title: Verzoek om verwijdering van gebruikersaccount
email.delete.description: De volgende gebruiker heeft verzocht om verwijdering
van zijn of haar account
resend_account_activation_email_question: Inactief account?
resend_account_activation_email: Stuur de accountactivatie-e-mail opnieuw
custom_css: Aangepaste CSS
ignore_magazines_custom_css: Negeer de aangepaste CSS op tijdschriften
oauth.consent.title: OAuth2-toestemmingsformulier
block: Blokkeren
unblock: Deblokkeren
oauth2.grant.read.general: Lees alle inhoud waartoe u toegang heeft.
oauth2.grant.write.general: Maak of bewerk uw discussies, berichten of
opmerkingen.
oauth2.grant.delete.general: Verwijder al uw discussies, berichten of
opmerkingen.
oauth2.grant.report.general: Rapporteer discussies, berichten of opmerkingen.
oauth2.grant.vote.general: Stem omhoog, omlaag of promoot discussies, berichten
of opmerkingen.
oauth2.grant.subscribe.general: Abonneer of volg een tijdschrift, domein of
gebruiker en bekijk de tijdschriften, domeinen en gebruikers waarop u zich
abonneert.
oauth2.grant.block.general: Blokkeer of deblokkeer een tijdschrift, domein of
gebruiker en bekijk de tijdschriften, domeinen en gebruikers die u hebt
geblokkeerd.
oauth2.grant.domain.subscribe: Schrijf u in of uit op domeinen en bekijk de
domeinen waarop u zich abonneert.
oauth2.grant.entry.create: Maak nieuwe discussies.
oauth2.grant.entry.edit: Bewerk uw bestaande discussies.
oauth2.grant.entry.delete: Verwijder uw bestaande discussies.
oauth2.grant.entry_comment.all: Maak, bewerk of verwijder uw opmerkingen in
discussies, en stem, promoot of rapporteer elke opmerking in een discussie.
oauth2.grant.entry_comment.create: Maak nieuwe opmerkingen in discussies.
oauth2.grant.entry_comment.delete: Verwijder uw bestaande opmerkingen in
discussies.
oauth2.grant.entry_comment.vote: Stem een reactie in een thread omhoog, omlaag
of boost.
oauth2.grant.magazine.block: Blokkeer of deblokkeer tijdschriften en bekijk de
tijdschriften die je hebt geblokkeerd.
oauth2.grant.post.create: Maak nieuwe berichten.
oauth2.grant.post.edit: Bewerk uw bestaande berichten.
oauth2.grant.post.delete: Verwijder uw bestaande berichten.
oauth2.grant.post.vote: Stem een bericht omhoog, omlaag of boost.
oauth2.grant.post.report: Rapporteer elk bericht.
oauth2.grant.post_comment.all: Maak, bewerk of verwijder uw opmerkingen over
berichten, en stem, promoot of rapporteer opmerkingen over een bericht.
oauth2.grant.post_comment.create: Maak nieuwe reacties op berichten.
oauth2.grant.post_comment.edit: Bewerk uw bestaande reacties op berichten.
oauth2.grant.post_comment.vote: Stem een reactie op een bericht omhoog, omlaag
of boost.
oauth2.grant.post_comment.report: Rapporteer elke reactie op een bericht.
oauth2.grant.user.profile.all: Lees en bewerk uw profiel.
oauth2.grant.user.profile.read: Lees je profiel.
oauth2.grant.user.profile.edit: Pas je profiel aan.
oauth2.grant.user.message.all: Lees uw berichten en stuur berichten naar andere
gebruikers.
oauth2.grant.user.message.read: Lees uw berichten.
oauth2.grant.user.message.create: Stuur berichten naar andere gebruikers.
oauth2.grant.user.notification.all: Lees en wis uw meldingen.
oauth2.grant.user.notification.delete: Wis uw meldingen.
oauth2.grant.user.oauth_clients.all: Lees en bewerk de machtigingen die u aan
andere OAuth2-applicaties hebt verleend.
oauth2.grant.user.oauth_clients.read: Lees de machtigingen die u aan andere
OAuth2-applicaties hebt verleend.
oauth2.grant.moderate.entry.change_language: Verander de taal van de discussies
in uw gemodereerde tijdschriften.
oauth2.grant.moderate.entry_comment.all: Modereer reacties in discussies in uw
gemodereerde tijdschriften.
federation_page_enabled: Federatiepagina ingeschakeld
federation_page_allowed_description: Bekende instanties waarmee we federeren
oauth2.grant.moderate.entry_comment.trash: Verwijder of herstel reacties in
discussies in uw gemodereerde tijdschriften.
oauth2.grant.moderate.post.all: Beheer berichten in uw gemodereerde
tijdsschriften.
oauth2.grant.moderate.magazine.all: Beheer verbannen, rapporteer en bekijk
verwijderde items in uw gemodereerde tijdschriften.
oauth2.grant.moderate.magazine.ban.read: Bekijk verbannen gebruikers in uw
gemodereerde tijdschriften.
oauth2.grant.moderate.magazine.ban.create: Verban gebruikers in uw gemodereerde
tijdschriften.
flash_post_pin_success: Het bericht is succesvol vastgezet.
flash_post_unpin_success: Het bericht is succesvol losgemaakt.
oauth2.grant.admin.entry_comment.purge: Verwijder alle reacties in een gesprek
volledig uit jouw instantie.
oauth2.grant.admin.magazine.all: Verplaats gesprekken tussen tijdschriften of
verwijder ze volledig op jouw instantie.
oauth2.grant.admin.post.purge: Verwijder elk bericht volledig uit jouw
instantie.
oauth2.grant.admin.post_comment.purge: Verwijder alle reacties op een bericht
volledig uit jouw instantie.
oauth2.grant.admin.magazine.move_entry: Verplaats gesprekken tussen
tijdschriften op jouw instantie.
oauth2.grant.admin.magazine.purge: Verwijder tijdschriften op jouw instantie
volledig.
oauth2.grant.admin.user.all: Gebruikers op jouw instantie verbannen, verifiëren
of volledig verwijderen.
oauth2.grant.admin.user.ban: Gebruikers van jouw instantie verbannen of de
verbanning opheffen.
oauth2.grant.admin.user.verify: Verifieer gebruikers op jouw instantie.
oauth2.grant.admin.user.delete: Verwijder gebruikers uit jouw instantie.
oauth2.grant.admin.user.purge: Verwijder gebruikers volledig uit jouw instantie.
oauth2.grant.admin.instance.all: Bekijk en update instantie instellingen of
informatie.
oauth2.grant.admin.instance.stats: Bekijk de statistieken van jouw instantie.
oauth2.grant.admin.instance.settings.all: Bekijk of update de instellingen op
jouw instantie.
oauth2.grant.admin.instance.settings.read: Bekijk de instellingen op jouw
instantie.
oauth2.grant.admin.instance.settings.edit: Update de instellingen op jouw
instantie.
oauth2.grant.admin.instance.information.edit: "Werk de pagina's: Over, Veelgestelde
vragen, Contact, Servicevoorwaarden en Privacybeleid bij op jouw instantie."
oauth2.grant.admin.federation.all: Bekijk en update momenteel gedefedereerde
instanties.
oauth2.grant.admin.federation.read: Bekijk de lijst met de-federatieve
instanties.
oauth2.grant.admin.federation.update: Voeg instanties toe aan of verwijder ze
uit de lijst met de-federatieve instanties.
oauth2.grant.admin.oauth_clients.all: Bekijk of trek OAuth2-clients in die op
jouw instanties bestaan.
oauth2.grant.admin.oauth_clients.read: Bekijk de OAuth2-clients die op jouw
instantie aanwezig zijn, en hun gebruiksstatistieken.
oauth2.grant.admin.oauth_clients.revoke: Trek de toegang tot OAuth2-clients op
jouw instantie in.
show_avatars_on_comments: Toon reactie profielfoto's
single_settings: Enkel
comment_reply_position_help: Toon het reactie-antwoordformulier bovenaan of
onderaan de pagina. Wanneer 'oneindig scrollen' is ingeschakeld, verschijnt de
positie altijd bovenaan.
update_comment: Reactie bijwerken
show_avatars_on_comments_help: Toon/verberg profielfoto's bij het bekijken van
reacties op een enkele discussie of post.
comment_reply_position: Commentaar reactie positie
magazine_theme_appearance_custom_css: Aangepaste CSS die van toepassing is bij
het bekijken van inhoud in uw tijdschrift.
magazine_theme_appearance_icon: Aangepast pictogram voor het tijdschrift.
magazine_theme_appearance_background_image: Aangepaste CSS die van toepassing is
bij het bekijken van inhoud in uw tijdschrift.
moderation.report.approve_report_title: Rapport Goedkeuren
moderation.report.reject_report_title: Rapport Afwijzen
moderation.report.ban_user_description: Wilt u de gebruiker (%username%)
verbannen die deze inhoud heeft gemaakt van dit tijdschrift?
moderation.report.approve_report_confirmation: Weet u zeker dat u dit rapport
wilt goedkeuren?
subject_reported_exists: Deze inhoud is al reeks gerapporteerd.
moderation.report.ban_user_title: Verban gebruiker
moderation.report.reject_report_confirmation: Weet u zeker dat u dit rapport
wilt afwijzen?
2fa.authentication_code.label: Authenticatiecode
2fa.remove: Verwijder 2FA
delete_content_desc: Verwijder de inhoud van de gebruiker maar laat de reacties
van andere gebruikers achter in de gemaakte discussies, berichten en
opmerkingen.
2fa.backup_codes.help: U kunt deze codes gebruiken als u niet over een apparaat
of app voor tweefactorauthenticatie beschikt. Je krijgt ze niet meer
te zien en je kunt ze enkel slechts één keer
gebruiken.
2fa.verify: Verifiëren
2fa.add: Voeg toe aan mijn account
2fa.disable: Schakel tweefactorauthenticatie uit
purge_content: Inhoud leegmaken
oauth2.grant.moderate.post.pin: Pin berichten bovenaan uw gemodereerde
tijdschriften.
2fa.backup_codes.recommendation: Het wordt aanbevolen dat u een kopie ervan op
een veilige plaats bewaart.
2fa.qr_code_img.alt: Een QR-code waarmee tweefactorauthenticatie voor uw account
ingesteld kan worden
delete_account_desc: Verwijder het account, inclusief de reacties van andere
gebruikers in gemaakte discussies, berichten en opmerkingen.
2fa.enable: Stel tweefactorauthenticatie in
2fa.user_active_tfa.title: Gebruiker heeft actieve 2FA
2fa.code_invalid: De authenticatiecode is niet geldig
2fa.available_apps: Gebruik een tweefactorauthenticatie-app zoals
%google_authenticator%, %aegis% (Android) of %raivo% (iOS) om de QR-code te
scannen.
cancel: Annuleren
purge_content_desc: De inhoud van de gebruiker volledig opschonen, inclusief het
verwijderen van de reacties van andere gebruikers in gemaakte discussies,
berichten en opmerkingen.
delete_content: Inhoud verwijderen
two_factor_backup: Back-upcodes voor tweefactorauthenticatie
password_and_2fa: Wachtwoord & 2FA
2fa.backup-create.label: Maak nieuwe back-upverificatiecodes
2fa.backup: Uw twee-factor back-upcodes
2fa.qr_code_link.title: Als u deze link bezoekt, kan uw platform deze
tweefactorauthenticatie mogelijk registreren
2fa.backup-create.help: U kunt nieuwe back-upauthenticatiecodes aanmaken; Als u
dit doet, worden bestaande codes ongeldig.
two_factor_authentication: Tweefactorauthenticatie
2fa.verify_authentication_code.label: Voer een tweefactorcode in om de
configuratie te verifiëren
flash_account_settings_changed: Uw accountinstellingen zijn succesvol gewijzigd.
U dient opnieuw in te loggen.
close: Sluiten
flash_post_new_success: Het bericht is succesvol aangemaakt.
flash_comment_edit_error: Kan reactie niet bewerken. Er is iets fout gegaan.
flash_comment_new_error: Kan reactie niet aanmaken. Er is iets fout gegaan.
flash_post_new_error: Bericht kon niet worden gemaakt. Er is iets fout gegaan.
flash_thread_new_error: Discussie kon niet worden gemaakt. Er is iets fout
gegaan.
flash_magazine_theme_changed_error: Kan het uiterlijk van het tijdschrift niet
bijwerken.
flash_post_edit_error: Kan bericht niet bewerken. Er is iets fout gegaan.
flash_post_edit_success: Bericht is succesvol bewerkt.
flash_user_edit_password_error: Kan wachtwoord niet wijzigen.
flash_magazine_theme_changed_success: Het uiterlijk van het tijdschrift is
bijgewerkt.
flash_thread_edit_error: Kan thread niet bewerken. Er is iets fout gegaan.
sidebars_same_side: Zijbalken aan dezelfde kant
flash_user_edit_email_error: Kan het e-mailadres niet wijzigen.
flash_comment_new_success: Reactie is succesvol aangemaakt.
flash_user_settings_general_error: Kan gebruikersinstellingen niet opslaan.
alphabetically: Alfabetisch
flash_email_was_sent: E-mail is succesvol verzonden.
show_subscriptions: Toon abonnementen
flash_comment_edit_success: Reactie is succesvol bijgewerkt.
flash_user_edit_profile_success: Gebruikersprofielinstellingen zijn succesvol
opgeslagen.
subscription_sort: Sorteren
flash_user_settings_general_success: Gebruikersinstellingen succesvol
opgeslagen.
pending: In behandeling
subscriptions_in_own_sidebar: In aparte zijbalk
flash_email_failed_to_sent: E-mail kon niet worden verzonden.
flash_user_edit_profile_error: Kan profielinstellingen niet opslaan.
page_width_fixed: Vast
page_width_auto: Auto
position_bottom: Onder
page_width_max: Max
page_width: Paginabreedte
position_top: Boven
change_my_cover: Verander mijn cover
open_url_to_fediverse: Originele URL openen
change_my_avatar: Verander mijn avatar
restore_magazine: Tijdschrift herstellen
account_settings_changed: Uw accountinstellingen zijn succesvol gewijzigd. U
dient opnieuw in te loggen.
suspend_account: Account opschorten
account_suspended: Het account is opgeschort.
subscription_header: Geabonneerde tijdschriften
deletion: Verwijdering
account_unbanned: Het account is gedeblokkeerd.
magazine_is_deleted: Tijdschrift is verwijderd. U kunt het binnen 30 dagen herstellen .
account_is_suspended: Gebruikersaccount is opschorting is opgeheven.
user_suspend_desc: Als u uw account opschort, wordt uw inhoud op de instantie
verborgen, maar niet permanent verwijderd. U kunt deze op elk gewenst moment
herstellen.
account_unsuspended: De account is niet langer opgeschort.
subscription_panel_large: Groot paneel
unsuspend_account: Account opschorting opheffen
account_banned: Het account is geblokkeerd.
delete_magazine: Tijdschrift verwijderen
subscription_sidebar_pop_out_right: Ga naar de aparte zijbalk aan de rechterkant
subscription_sidebar_pop_out_left: Ga naar de aparte zijbalk aan de linkerkant
purge_magazine: Tijdschrift opruimen
magazine_deletion: Tijdschrift verwijderen
subscription_sidebar_pop_in: Verplaats abonnementen naar het inline paneel
edit_my_profile: Bewerk mijn profiel
user_badge_moderator: Mod
default_theme: Standaard thema
remove_following: Verwijder volgen
mark_as_adult: Markeer NSFW
user_badge_admin: Admin
2fa.setup_error: Fout bij het inschakelen van 2FA voor account
user_badge_global_moderator: Globale Mod
apply_for_moderator: Solliciteer als moderator
unmark_as_adult: Onmarkeer NSFW
deleted_by_author: Gesprek, bericht of reactie is verwijderd door de auteur
solarized_auto: Gesolariseerd (Automatische detectie)
announcement: Aankondiging
sensitive_toggle: Schakel de zichtbaarheid van gevoelige inhoud in of uit
sensitive_show: Klik om te tonen
user_badge_bot: Bot
ownership_requests: Eigendomsverzoeken
sensitive_warning: Gevoelige inhoud
keywords: Trefwoorden
moderator_requests: Mod verzoeken
default_theme_auto: Licht/Donker (Automatische detectie)
action: Actie
flash_mark_as_adult_success: Het bericht is succesvol gemarkeerd als NSFW.
cancel_request: Annuleer verzoek
deleted_by_moderator: Gesprek, bericht of reactie is verwijderd door de
moderator
flash_unmark_as_adult_success: Het bericht is met succes ongemarkeerd als NSFW.
abandoned: Verlaten
remove_subscriptions: Verwijder abonnementen
sensitive_hide: Klik om te verbergen
request_magazine_ownership: Vraag tijdschrifteigendom aan
accept: Accepteer
user_badge_op: OP
subscribers_count: '{0}Abonnees|{1}Abonnee|]1,Inf[ Abonnees'
menu: Menu
details: Details
followers_count: '{0}Volgers|{1}Volger|]1,Inf[ Volgers'
remove_media: Media verwijderen
all_time: Alle tijden
spoiler: Spoiler
show: Tonen
hide: Verbergen
edited: bewerkt
disconnected_magazine_info: Dit tijdschrift heeft geen updates ontvangen
(laatste activiteit %days% dag(en) geleden).
subscribe_for_updates: Abonneer om updates te ontvangen.
sso_registrations_enabled: SSO -registraties ingeschakeld
sso_registrations_enabled.error: Nieuwe accountregistraties met
identiteitsbeheerders van derden zijn momenteel uitgeschakeld.
always_disconnected_magazine_info: Dit tijdschrift heeft geen updates ontvangen.
marked_for_deletion_at: Gemarkeerd voor verwijdering op %date%
account_deletion_title: Accountverwijdering
account_deletion_button: Account verwijderen
account_deletion_immediate: Onmiddellijk verwijderen
marked_for_deletion: Gemarkeerd voor verwijdering
account_deletion_description: Uw account wordt binnen 30 dagen verwijderd,
tenzij u ervoor kiest om het account onmiddellijk te verwijderen. Om uw
account binnen 30 dagen te herstellen, logt u in met dezelfde
gebruikersreferenties of neemt u contact op met een beheerder.
remove_schedule_delete_account_desc: Verwijder de geplande verwijdering. Alle
inhoud is weer beschikbaar en de gebruiker kan inloggen.
schedule_delete_account: Plan Verwijdering
schedule_delete_account_desc: Plan de verwijdering van dit account over 30
dagen. Hierdoor worden de gebruiker en zijn inhoud verborgen en kan de
gebruiker niet inloggen.
remove_schedule_delete_account: Geplande verwijdering verwijderen
from: van
ban_hashtag_description: Als je een hashtag verbiedt, dan worden er geen
berichten met deze hashtag meer aangemaakt en tevens worden bestaande
berichten met deze hashtag verborgen.
restrict_magazine_creation: Beperk het maken van lokale tijdschriften tot
beheerders en algemene moderatoren
direct_message: Direct bericht
magazine_log_mod_added: heeft een moderator toegevoegd
magazine_log_mod_removed: heeft een moderator verwijderd
tag: Label
unban: Ban opheffen
ban_hashtag_btn: Ban Hastag
unban_hashtag_btn: Opheffen Hashtag Ban
unban_hashtag_description: Als je een ban van een hashtag ongedaan maakt, dan
kun je weer berichten met deze hashtag aanmaken. Bestaande berichten met deze
hashtag zijn niet langer verborgen.
private_instance: Dwing gebruikers om in te loggen voordat ze toegang krijgen
tot inhoud
flash_thread_tag_banned_error: Discussie kon niet worden aangemaakt. Deze inhoud
is niet toegestaan.
filter_labels: Filter labels
sso_only_mode: Beperk inloggen en registratie alleen tot SSO (Single-Sign-On)
methoden
related_entry: Gerelateerd
cake_day: Taart dag
someone: Iemand
last_updated: Laatst bijgewerkt
sort_by: Sorteer op
filter_by_subscription: Filter op abonnement
filter_by_federation: Filter op federatiestatus
auto: Automatisch
back: Terug
and: en
sso_show_first: Toon SSO (Single-Sign-On) als eerste op de inlog-en
registratiepagina's
continue_with: Daargaan met
magazine_log_entry_pinned: vastgezette gesprek
magazine_log_entry_unpinned: verwijderde vastgezette gesprek
own_content_reported_accepted: Een melding over uw inhoud was geaccepteerd.
open_report: Open melding
report_accepted: Een melding was geaccepteerd
show_related_entries: Toon willekeurige gesprekken
show_related_magazines: Toon willekeurige tijdschriften
show_related_posts: Toon willekeurige berichten
show_active_users: Toon actieve gebruikers
federation_page_dead_title: Dode instanties
federation_page_dead_description: Instanties waarin we niet minimaal 10
activiteiten achter elkaar konden leveren en waarbij de laatste succesvolle
aflevering en ontvangst meer dan een week geleden was
reporting_user: Rapporterende gebruiker
reported: gerapporteerd
report_subject: Onderwerp
own_report_rejected: Uw melding was afgewezen
own_report_accepted: Uw rapport is geaccepteerd
manually_approves_followers: Volgers handmatig goedkeuren
register_push_notifications_button: Registreer voor Pushmeldingen
unregister_push_notifications_button: Verwijder Push registratie
test_push_notifications_button: Test pushmeldingen
test_push_message: Hallo Wereld!
notification_title_new_comment: New commentaar
notification_title_mention: Je werd genoemd
notification_title_new_reply: Nieuw Antwoord
notification_title_new_thread: Nieuw gesprek
notification_title_new_report: Een nieuwe melding is aangemaakt
flash_posting_restricted_error: Het maken van gesprekken is beperkt tot
moderators van dit tijdschrift en jij bent er geen van
server_software: Server software
version: Versie
reported_user: Gerapporteerde gebruiker
notification_title_removed_comment: Een opmerking is verwijderd
notification_title_ban: Je bent verbannen
notification_title_removed_post: Een bericht is verwijderd
notification_title_removed_thread: Een gesprek is verwijderd
notification_title_message: Nieuw direct bericht
notification_title_edited_comment: Een opmerking is gewijzigd
notification_title_new_post: Nieuw Bericht
notification_title_edited_thread: Een gesprek is gewijzgd
notification_title_edited_post: Een bericht is gewijzigd
magazine_posting_restricted_to_mods_warning: Enkel moderators kunnen een nieuw
gesprek aanmaken in dit tijdschrift
last_successful_deliver: Laatste succesvolle levering
last_successful_receive: Laatste succesvolle ontvangst
last_failed_contact: Laatste mislukte contact
magazine_posting_restricted_to_mods: Beperk het aanmaken van gesprekken tot
moderators
new_user_description: Deze gebruiker is nieuw (actief voor minder dan %days%
dagen)
new_magazine_description: Dit tijdschrift is nieuw (actief voor minder dan
%days% dagen)
flash_image_download_too_large_error: Afbeelding kon niet worden aangemaakt,
deze is te groot (max. grootte %bytes%)
admin_users_banned: Geband
max_image_size: Maximale bestandsgrootte
user_verify: Account activeren
admin_users_active: Actief
admin_users_inactive: Inactief
admin_users_suspended: Opgeschort
edit_entry: Bewerk gesprek
downvotes_mode: Omlaagstem-modus
change_downvotes_mode: Wijzig omlaagstem modus
disabled: Uitgeschakeld
hidden: Verborgen
enabled: Ingeschakeld
toolbar.spoiler: Spoiler
comment_not_found: Commentaar niet gevonden
table_of_contents: Inhoudsopgave
notification_body2_new_signup_approval: Je moet het verzoek goedkeuren voordat
ze kunnen inloggen
bookmark_remove_from_list: Bladwijzer verwijderen uit %list%
bookmark_remove_all: Verwijder alle bladwijzers
bookmark_add_to_default_list: Bladwijzer toevoegen aan standaardlijst
bookmark_lists: Bladwijzerlijst
bookmarks_list: Bladwijzers in %list%
count: Aantal
is_default: Is Standaard
bookmark_list_create: Aanmaken
bookmark_list_create_placeholder: type naam...
bookmark_list_create_label: Lijstnaam
bookmarks_list_edit: Bladwijzerlijst bewerken
bookmark_list_edit: Bewerken
bookmark_list_selected_list: Geselecteerde lijst
search_type_all: Alles
search_type_post: Microblogs
select_user: Kies een gebruiker
new_users_need_approval: Nieuwe gebruikers moeten door een beheerder worden
goedgekeurd voordat ze kunnen inloggen.
signup_requests: Aanmeldingsverzoeken
application_text: Leg uit waarom je lid wilt worden
signup_requests_header: Aanmeldingsverzoeken
flash_application_info: Een beheerder moet uw account goedkeuren voordat u kunt
inloggen. U ontvangt een e-mail zodra uw registratieverzoek is verwerkt.
email_application_approved_title: Uw registratieverzoek is goedgekeurd
email_application_rejected_title: Uw registratieverzoek is afgewezen
email_application_rejected_body: Hartelijk dank voor uw interesse, maar helaas
moeten wij u mededelen dat uw aanmeldingsverzoek is afgewezen.
email_application_pending: Voordat u kunt inloggen, is goedkeuring van de
beheerder nodig.
email_verification_pending: U moet uw e-mailadres verifiëren voordat u kunt
inloggen.
your_account_is_not_yet_approved: Uw account is nog niet goedgekeurd. We sturen
u een e-mail zodra de beheerders uw registratieverzoek hebben verwerkt.
notification_title_new_signup: Een nieuwe gebruiker heeft zich geregistreerd
bookmark_add_to_list: Bladwijzer toevoegen aan %list%
notification_body_new_signup: De gebruiker %u% heeft zich geregistreerd.
bookmarks: Bladwijzers
bookmark_list_is_default: Is standaard lijst
search_type_entry: Discussies
signup_requests_paragraph: Deze gebruikers willen graag lid worden van uw
server. Ze kunnen niet inloggen totdat je hun registratieverzoeken hebt
goedgekeurd.
email_application_approved_body: Uw registratieverzoek is goedgekeurd door de
server admins. U kunt nu inloggen op de server op %siteName% .
notify_on_user_signup: Nieuwe aanmeldingen
bookmark_list_make_default: Maak Standaard
oauth2.grant.user.bookmark_list.delete: Verwijder jouw bladwijzerlijsten
compact_view_help: Een compacte weergave met minder marges, waarbij de media
naar de rechterkant is verplaatst.
show_thumbnails_help: Toon de miniatuurafbeeldingen.
show_magazines_icons_help: Geef het tijdschrifticoon weer.
image_lightbox_in_list_help: Als het is aangevinkt, wordt door te klikken op de
miniatuur een dialoogvenster met een afbeeldingsvak weergegeven. Als het niet
is aangevinkt, wordt door te klikken op de miniatuur de thread geopend.
viewing_one_signup_request: Je bekijkt slechts één registratieverzoek van
%username%
front_default_sort: Standaard sortering op voorpagina
by: op
show_users_avatars_help: Geef de avatarafbeelding van de gebruiker weer.
oauth2.grant.user.bookmark: Bladwijzers toevoegen en verwijderen
oauth2.grant.user.bookmark.add: Bladwijzers toevoegen
oauth2.grant.user.bookmark.remove: Bladwijzers verwijderen
oauth2.grant.user.bookmark_list: Lees, bewerk en verwijder jouw
bladwijzerlijsten
oauth2.grant.user.bookmark_list.read: Lees jouw bladwijzerlijsten
oauth2.grant.user.bookmark_list.edit: Bewerk jouw bladwijzerlijsten
show_magazine_domains: Toon domeinen tijdschrift
answered: beantwoord
comment_default_sort: Standaard sortering op opmerkingen
open_signup_request: Open inschrijvingsverzoek
image_lightbox_in_list: Thread-miniaturen openen volledig scherm
show_user_domains: Gebruikersdomeinen weergeven
remove_user_avatar: Avatar verwijderen
remove_user_cover: Cover verwijderen
show_new_icons: Laat "nieuw" pictogrammen zien
show_new_icons_help: Toon pictogram voor nieuw tijdschrift/gebruiker (30 dagen
oud of nieuwer)
toolbar.emoji: Emoji
2fa.manual_code_hint: Als u de QR-code niet kunt scannen, voer dan het
geheimcode handmatig in
crosspost: Kruistpost
banner: Banner
type_search_term_url_handle: Typ zoekterm, url of gebruikersnaam
magazine_theme_appearance_banner: Aangepaste banner voor het tijdschrift. Deze
wordt boven alle discussies weergegeven en moet een brede beeldverhouding
hebben (5:1, of 1500px * 300px).
flash_thread_ref_image_not_found: De afbeelding waarnaar 'imageHash' verwijst,
kon niet worden gevonden.
search_type_magazine: Tijdschriften
search_type_user: Gebruikers
search_type_actors: Tijdschriften + Gebruikers
search_type_content: Discussies + Microblogs
magazine_instance_defederated_info: De instantie van dit tijdschrift is
gedefedereerd. Het tijdschrift ontvangt daarom geen updates.
user_instance_defederated_info: De instantie van deze gebruiker is
gedefedereerd.
flash_thread_instance_banned: De instantie van dit tijdschrift is verbannen.
show_rich_mention: Rijke vermeldingen
show_rich_mention_help: Geef een gebruikerscomponent weer wanneer een gebruiker
wordt genoemd. Dit omvat de weergavenaam en profielfoto.
show_rich_mention_magazine: Rijke tijdschriften vermeldingen
show_rich_mention_magazine_help: Geef een tijdschriftcomponent weer wanneer een
tijdschrift wordt genoemd. Dit omvat de weergavenaam en het pictogram.
show_rich_ap_link: Rijke AP-koppelingen
show_rich_ap_link_help: Geef een inline-component weer wanneer er naar andere
ActivityPub-inhoud wordt gelinkt.
attitude: Houding
type_search_magazine: Beperk uw zoekopdracht tot tijdschrift...
type_search_user: Beperk zoekopdracht tot auteur...
modlog_type_entry_deleted: Discussie is verwijderd
modlog_type_entry_restored: Discussie is hersteld
modlog_type_entry_comment_deleted: Discussie commentaar is verwijderd
modlog_type_entry_comment_restored: Discussie commentaar is hersteld
modlog_type_entry_pinned: Discussie vastgezet
modlog_type_entry_unpinned: Discussie losmaken
modlog_type_post_deleted: Microblog verwijderd
modlog_type_post_restored: Microblog hersteld
modlog_type_post_comment_deleted: Microblog-antwoord verwijderd
modlog_type_post_comment_restored: Microblog-antwoord hersteld
modlog_type_ban: Gebruiker verbannen uit tijdschrift
modlog_type_moderator_add: Moderator voor het tijdschrift toegevoegd
modlog_type_moderator_remove: Magazine moderator verwijderd
everyone: Iedereen
nobody: Niemand
followers_only: Alleen volgers
direct_message_setting_label: Wie kan jou een direct bericht sturen
delete_magazine_icon: Tijdschriftpictogram verwijderen
flash_magazine_theme_icon_detached_success: Tijdschriftpictogram succesvol
verwijderd
delete_magazine_banner: Verwijder tijdschriftbanner
flash_magazine_theme_banner_detached_success: Tijdschriftbanner succesvol
verwijderd
federation_uses_allowlist: Gebruik een toegestane lijst voor federatie
defederating_instance: Defederatie-instantie %i
their_user_follows: Aantal gebruikers van hun instantie die gebruikers op onze
instantie volgen
our_user_follows: Aantal gebruikers van onze instantie dat gebruikers op hun
instantie volgen
their_magazine_subscriptions: Aantal gebruikers van hun instantie dat zich heeft
geabonneerd op tijdschriften op onze instantie
our_magazine_subscriptions: Aantal gebruikers op ons instantie dat zich heeft
geabonneerd op tijdschriften op hun instantie
confirm_defederation: Bevestig de defederatie
flash_error_defederation_must_confirm: U moet de defederatie bevestigen
allowed_instances: Toegestane instanties
btn_deny: Weigeren
btn_allow: Toestaan
ban_instance: Ban instantie
allow_instance: Sta instantie toe
federation_page_use_allowlist_help: Als een toegestane lijst wordt gebruikt, zal
deze instantie alleen federeren met de expliciet toegestane instanties. Anders
zal deze instantie federeren met elke instantie, behalve met instanties die
geblokkeerd zijn.
front_default_content: Standaardweergave voorpagina
default_content_default: Serverstandaard (Gesprekken)
default_content_microblog: Microblog
default_content_threads: Gesprekken
combined: Gecombineerd
sidebar_sections_random_local_only: Beperk de 'Willekeurige threads/berichten'
in de zijblak tot alleen lokaal
sidebar_sections_users_local_only: Beperk de 'Actieve mensen' in de zijbalk tot
alleen lokaal
random_local_only_performance_warning: Het inschakelen van 'Alleen willekeurig
lokaal' kan gevolgen hebben voor de SQL prestaties.
default_content_combined: Gesprekken + Microblog
ban_expires: Ban vervalt
you_have_been_banned_from_magazine: Je bent verbannen uit het tijdschrift %m.
you_have_been_banned_from_magazine_permanently: Je bent permanent verbannen uit
het tijdschrift %m.
you_are_no_longer_banned_from_magazine: Je bent niet langer verbannen uit
tijdschrift %m.
================================================
FILE: translations/messages.pl.yaml
================================================
type.photo: Obraz
done: Gotowe
type.link: Link
type.video: Video
image: Obraz
1y: 1r
expand: Rozwiń
error: Błąd
icon: Ikona
pin: Przypnij
unpin: Odepnij
month: Miesiąc
message: Wiadomość
infinite_scroll: Nieskończone przewijanie
show_top_bar: Pokaż top bar
sticky_navbar: Przyklejony pasek nawigacyjny
subject_reported: Treść została zgłoszona.
sidebar_position: Pozycja sidebara
left: Lewo
right: Prawo
federation: Federacja
status: Status
on: Wł
off: Wył
instances: Instancje
upload_file: Dołącz plik
from_url: Dołącz url
magazine_panel: Panel magazynu
reject: Odrzuć
approve: Zatwierdź
ban: Ban
filters: Filtry
approved: Zatwierdzone
rejected: Odrzucone
add_moderator: Dodaj moderatora
add_badge: Dodaj etykietę
bans: Bany
created: Utworzono
expires: Wygasa
perm: Perm
expired_at: Wygasa
add_ban: Dodaj ban
reports: Zgłoszenia
notifications: Powiadomienia
messages: Wiadomości
appearance: Wygląd
homepage: Strona główna
hide_adult: Ukryj treści dla do dorosłych
featured_magazines: Polecane magazyny
privacy: Prywatność
show_profile_subscriptions: Pokaż w profilu obserwowane magazyny
show_profile_followings: Pokaż w profilu obserwowanych ludzi
notify_on_new_entry_reply: Powiadom mnie o nowym komentarzu w moich treściach
notify_on_new_entry_comment_reply: Powiadom mnie o odpowiedzi na moje komentarze
w treściach
notify_on_new_post_reply: Powiadom mnie o odpowiedzi na moje posty
notify_on_new_post_comment_reply: Powiadom mnie o odpowiedzi na moje komentarze
na mikroblogu
notify_on_new_entry: Powiadom mnie o nowych treściach w obserwowanych magazynach
notify_on_new_posts: Powiadom mnie o nowych postach w obserwowanych magazynach
save: Zapisz
about: Notka
old_email: Stary email
new_email: Nowy email
new_email_repeat: Powtórz nowy email
current_password: Obecne hasło
new_password: Nowe hasło
new_password_repeat: Powtórz nowe hasło
change_email: Zmień email
change_password: Zmień hasło
collapse: Zwiń
domains: Domeny
votes: Głosy
theme: Wygląd
dark: Ciemny
light: Jasny
solarized_light: Solarized Light
solarized_dark: Solarized Dark
font_size: Rozmiar czcionki
size: Rozmiar
boosts: Podbicia
yes: Tak
no: Nie
show_magazines_icons: Pokaż ikony magazynów
show_thumbnails: Pokaż miniaturki
rounded_edges: Zaokrąglone rogi
removed_thread_by: Usunął treść utworzoną przez
removed_comment_by: usunął komentarz utworzony przez
restored_comment_by: przywrócił komentarz utworzony przez
removed_post_by: usunął post utworzony przez
restored_post_by: przywrócił post utworzony przez
he_banned: ban
he_unbanned: odbanuj
show_all: Pokaż wszystko
flash_thread_new_success: Treść została poprawnie utworzona i za chwilę będzie
widoczna dla innych.
flash_thread_edit_success: Treść została poprawnie zedytowana.
flash_thread_delete_success: Treść została poprawnie usunięta.
flash_thread_pin_success: Treść została przypięta.
flash_thread_unpin_success: Treść została odpięta.
flash_magazine_edit_success: Magazyn został poprawnie zedytowany.
too_many_requests: Limit wyczerpany, sprawdź ponownie później.
set_magazines_bar: Pasek magazynów
set_magazines_bar_desc: podaj nazwy magazynów oddzielone przecinkiem
set_magazines_bar_empty_desc: jeżeli pole jest puste, na pasku wyświetlane będą
aktywne magazyny.
trash: Kosz
type.magazine: Magazyn
thread: Treść
threads: Treści
microblog: Mikroblog
people: Ludzie
events: Wydarzenia
magazine: Magazyn
search: Szukaj
add: Dodaj
select_channel: Wybierz kanał
login: Zaloguj
top: Ważne
hot: Gorące
active: Aktywne
newest: Najnowsze
oldest: Najstarsze
commented: Komentowane
change_view: Zmień widok
filter_by_type: Filtruj po typie
comments_count: '{0}Komentarzy|{1}Komentarz|]1,Inf[ Komentarze'
favourites: Ulubione
favourite: Ulubione
more: Więcej
avatar: Avatar
added: Dodano
down_votes: Minusy
no_comments: Brak komentarzy
created_at: Utworzono
owner: Właściciel
subscribers: Obserwujący
online: Online
comments: Komentarze
posts: Wpisy
replies: Odpowiedzi
mod_log: Log moderatorski
add_comment: Dodaj komentarz
add_post: Dodaj post
add_media: Dodaj media
markdown_howto: Jak działa edytor?
enter_your_comment: Napisz komentarz
enter_your_post: Napisz post
activity: Aktywność
cover: Okładka
random_posts: Losowe wpisy
federated_magazine_info: Magazyn ze zdalnego serwera może być niekompletny.
go_to_original_instance: Zobacz więcej na oryginalnej instancji.
empty: Pusto
subscribe: Subskrybuj
follow: Obserwuj
unsubscribe: Subskrybujesz
unfollow: Obserwujesz
reply: Odpowiedz
password: Hasło
remember_me: Zapamiętaj mnie
you_cant_login: Nie pamiętasz hasła?
already_have_account: Masz już konto?
register: Zarejestruj
reset_password: Przypomnij hasło
show_more: Zobacz więcej
to: do
in: w
username: Nazwa użytkownika
email: Email
terms: Regulamin
privacy_policy: Polityka prywatności
about_instance: O instancji
all_magazines: Wszystkie magazyny
stats: Statystyki
fediverse: Fediwersum
create_new_magazine: Utwórz nowy magazyn
add_new_video: Dodaj video
add_new_article: Dodaj nowy wątek
add_new_link: Dodaj link
add_new_post: Dodaj wpis na mikroblogu
contact: Kontakt
faq: FAQ
rss: RSS
change_theme: Zmień wygląd
useful: Przydatne
help: Pomoc
check_email: Sprawdź swój mail
reset_check_email_desc2: Jeśli nie otrzymasz e-maila, sprawdź folder ze spamem.
try_again: Spróbuj ponownie
up_vote: Podbij
down_vote: Zminusuj
email_confirm_header: Cześć! Potwierdź adres email.
email_confirm_content: 'Chcesz aktywować swoje konto Mbin? Kliknij poniższy link:'
email_verify: Potwierdź adres email
email_confirm_expire: Link wygaśnie za godzinę.
email_confirm_title: Potwierdź adres email.
select_magazine: Wybierz magazyn
add_new: Dodaj nowy
url: URL
title: Tytuł
body: Treść
tags: Tagi
badges: Etykiety
is_adult: +18 / NSFW
eng: ENG
oc: OC
add_new_photo: Dodaj obraz
name: Nazwa
description: Opis
rules: Zasady
domain: Domena
followers: Obserwujący
following: Obserwowani
overview: Przegląd
cards: Karty
columns: Kolumny
user: Użytkownik
joined: Dołączył
moderated: Moderowane
people_local: Lokalne
people_federated: Sfederowane
related_tags: Powiązane tagi
go_to_content: Przejdź to treści
go_to_filters: Przejdź do filtrów
go_to_search: Przejdź do wyszukiwarki
subscribed: Subskrybowane
all: Wszystkie
logout: Wyloguj
compact_view: Widok kompaktowy
chat_view: Widok czatu
tree_view: Widok drzewa
table_view: Widok tabeli
cards_view: Widok kart
3h: 3g
6h: 6g
12h: 12g
1d: 1d
1w: 1t
1m: 1m
links: Linki
articles: Wątki
photos: Obrazy
videos: Video
report: Zgłoś
share: Udostępnij
copy_url: Kopiuj link Mbin
share_on_fediverse: Podziel się w Fediwersum
edit: Edytuj
are_you_sure: Jesteś pewien?
moderate: Moderuj
reason: Powód
delete: Usuń
edit_post: Edytuj post
settings: Ustawienia
general: Ogólne
profile: Profil
blocked: Blokady
edited_thread: Edytował/a treść
mod_remove_your_thread: Moderator usunął twoją treść
added_new_thread: Dodał/a nową treść
added_new_comment: Dodał/a nowy komentarz
edited_comment: Edytował/a komentarz
replied_to_your_comment: Odpowiedział/a na twój komentarz
mod_deleted_your_comment: Moderator usunął twój komentarz
added_new_post: Dodał/a nowy post
edited_post: Edytował/a post
added_new_reply: Dodał/a nową odpowiedź
wrote_message: Napisał/a wiadomość
banned: Zbanował/a cię
removed: Usunięte przez moderację
deleted: Usunięte przez autora
mentioned_you: Wspomniał cię
comment: Komentarz
post: Post
purge: Wyczyść
send_message: Wyślij wiadomość
type.article: Artykuł
type.smart_contract: Inteligentny kontrakt
magazines: Magazyny
filter_by_time: Wybierz zakres
up_votes: Podbicia
moderators: Moderatorzy
related_posts: Powiązane posty
federated_user_info: Profil ze zdalnego serwera może być niekompletny.
login_or_email: Login lub email
dont_have_account: Nie masz konta?
repeat_password: Powtórz hasło
agree_terms: Zgadzam się z %terms_link_start%Regulaminem%terms_link_end% i
%policy_link_start%Polityką prywatności%policy_link_end%
reset_check_email_desc: Jeśli już istnieje konto powiązane z twoim adresem
email, to wkrótce otrzymasz email zawierający link, który możesz użyć do
zresetowania hasła. Ten link wygaśnie za %expire%.
image_alt: Alternatywny tekst opisujący obraz
subscriptions: Subskrypcje
reputation_points: Punkty reputacji
change_language: Zmień język
change: Zmień
pinned: Przypięty
preview: Podgląd
article: Artykuł
reputation: Reputacja
note: Notatka
writing: Pisanie
users: Ludzie
content: Treści
week: Tydzień
weeks: Tygodnie
months: Miesiące
year: Rok
federated: Sfederowane
local: Lokalne
admin_panel: Panel administratora
dashboard: Dashboard
contact_email: Email kontaktowy
meta: Meta
instance: Instancja
pages: Strony
FAQ: FAQ
type_search_term: Wpisz frazę wyszukiwania
federation_enabled: Federacją włączona
registrations_enabled: Rejestracja włączona
restore: Przywróć
add_mentions_entries: Dodaj oznaczenia użytkowników w treściach
add_mentions_posts: Dodaj oznaczenia użytkowników na mikroblogu
Password is invalid: Hasło jest nieprawidłowe.
Your account has been banned: Twoje konto zostało zbanowane.
classic_view: Widok klasyczny
copy_url_to_fediverse: Kopiuj link (Fediwersum)
edit_comment: Zapisz zmiany
show_users_avatars: Pokaż avatary użytkowników
restored_thread_by: przywrócił treść utworzoną przez
read_all: Przeczytaj wszystkie
flash_register_success: Twoje konto zostało zarejestrowane. Sprawdź swoją
skrzynkę e-mail, wysłaliśmy wiadomość z linkiem aktywacyjnym.
flash_magazine_new_success: Magazyn został poprawnie utworzony. Możesz dodawać
nowe treści lub zapoznać się z panelem administratora.
mod_log_alert: W modlogu możesz trafić na drastyczne treści usunięte przez
moderatorów. Upewnij się, że wiesz co robisz...
mod_remove_your_post: Moderator usunął twój post
ban_expired: Ban wygasa
change_magazine: Zmień magazyn
registration_disabled: Rejestracja wyłączona
Your account is not active: Twoje konto nie jest aktywne.
send: Wyślij
firstname: Imię
active_users: Ostatnio aktywni
random_entries: Losowe treści
related_entries: Powiązane treści
delete_account: Usuń konto
purge_account: Wyczyść konto
ban_account: Zbanuj konto
unban_account: Odbanuj konto
related_magazines: Powiązane magazyny
random_magazines: Losowe magazyny
magazine_panel_tags_info: Wprowadź tylko wtedy, gdy chcesz żeby treści z
fediwersum były umieszczane w tym magazynie na podstawie tagów
sidebar: Menu boczne
auto_preview: Pogląd mediów
dynamic_lists: Dynamiczne listy
banned_instances: Zbanowane instancje
kbin_intro_title: Zanurkuj w Fediwersum
kbin_intro_desc: to zdecentralizowana platforma służąca do agregowania treści i
mikroblogowania, działająca w sieci Fediverse.
kbin_promo_title: Utwórz własną instancję
kbin_promo_desc: '%link_start%Sklonuj repo%link_end% i rozwijaj fediverse'
captcha_enabled: Captcha włączona
header_logo: Logo w nagłówku
return: Wróć
browsing_one_thread: Przeglądasz tylko jeden wątek w dyskusji! Wszystkie
Komentarze dostępne są na stronie posta.
boost: Podbij
report_issue: Zgłoś błąd
mercure_enabled: Włączono Mercure
tokyo_night: Tokyo Nocą
preferred_languages: Filtruj język wątków i wpisów
infinite_scroll_help: Automatycznie wczytuj więcej treści, gdy dotrzesz do końca
strony.
sticky_navbar_help: Pasek nawigacyjny przyczepi się do góry strony, gdy
przewijasz w dół.
auto_preview_help: Automatycznie pokazuj podgląd mediów.
reload_to_apply: Przeładuj stronę, żeby zaakceptować zmiany
filter.origin.label: Wybierz pochodzenie
filter.fields.label: Wybierz, które pola przeszukać
filter.adult.only: Tylko NSFW
filter.fields.only_names: Tylko nazwy
filter.fields.names_and_descriptions: Nazwy i opis
filter.adult.label: Wybierz, czy wyświetlać treści NSFW
filter.adult.hide: Ukryj NSFW
filter.adult.show: Pokaż NSFW
local_and_federated: Lokalne i sfederowane
bot_body_content: "Witaj w Bocie Mbin! Ten bot odgrywa kluczową rolę w umożliwianiu
funkcji ActivityPub w obrębie Mbin. Zapewnia, że Mbin może komunikować się i federować
z innymi instancjami w fediverse.\n\nActivityPub to otwarty standardowy protokół,
który umożliwia komunikację i interakcje między zdecentralizowanymi platformami
społecznościowymi. Umożliwia użytkownikom na różnych instancjach (serwerach) śledzenie,
współdziałanie i udostępnianie treści w ramach federowanej sieci społecznościowej
znanej jako fediverse. Daje standaryzowany sposób na publikowanie treści, śledzenie
innych użytkowników oraz angażowanie się w interakcje społeczne, takie jak polubienia,
udostępnianie i komentowanie wątków lub wpisów."
kbin_bot: Bot Mbin
password_confirm_header: Potwierdzi żądanie zmiany hasła.
your_account_is_not_active: Twoje konto nie zostało aktywowane. Proszę sprawdź
swój email i kliknij na link aktywacyjny, żeby kontynuować
your_account_has_been_banned: Twoje konto zostało zbanowane
toolbar.bold: Pogrubienie
toolbar.italic: Kursywa
toolbar.header: Nagłówek
toolbar.quote: Cytat
toolbar.code: Kod
toolbar.link: Link
toolbar.image: Obraz
toolbar.unordered_list: Lista punktowana
toolbar.ordered_list: Lista numerowana
toolbar.mention: Wzmianka
toolbar.strikethrough: Przekreślenie
more_from_domain: Więcej z domeny
errors.server404.title: 404 Nie znaleziono
federation_page_enabled: Strona federacji włączona
flash_post_unpin_success: Wpis został odpięty.
flash_post_pin_success: Wpis został przypięty.
2fa.authentication_code.label: Kod uwierzytelniający
2fa.remove: Usuń 2FA
2fa.verify: Weryfikuj
last_active: Ostatnia aktywność
oauth.consent.allow: Zezwalaj
custom_css: Niestandardowy CSS
block: Zablokuj
errors.server403.title: 403 Dostęp zabroniony
oauth.consent.deny: Odrzuć
single_settings: Pojedynczy
moderation.report.ban_user_title: Zablokuj użytkownika
resend_account_activation_email_question: Nieaktywne konto?
cancel: Anuluj
delete_content: Usuń zawartość
unblock: Odblokuj
oauth.consent.grant_permissions: Udziel uprawnienia
================================================
FILE: translations/messages.pt.yaml
================================================
type.link: Ligação
type.article: Tópico
type.photo: Foto
type.video: Video
type.magazine: Magazine
thread: Tópico
threads: Tópicos
microblog: Microblog
people: Pessoas
events: Eventos
magazine: Magazine
magazines: Magazines
search: Procura
add: Adicionar
select_channel: Selecionar um canal
login: Log in
top: Topo
hot: Quente
newest: Novo
oldest: Antigo
commented: Comentado
change_view: Alterar vista
filter_by_time: Filtrar por tempo
filter_by_type: Filtrar por tipo
favourites: Votos a favor
favourite: Favorito
more: Mais
avatar: Avatar
added: Adicionado
up_votes: Upvoto
down_votes: Downvoto
no_comments: Sem comentários
created_at: Criado
owner: Dono
subscribers: Subscritores
online: Online
comments: Comentários
posts: Postagens
moderators: Moderadores
mod_log: Log de Moderação
add_comment: Adicionar comentário
add_post: Adicionar postagem
add_media: Adicionar media
enter_your_comment: Introduza o seu comentário
enter_your_post: Introduza a sua postagem
activity: Atividade
cover: Capa
related_posts: Postagens relacionadas
random_posts: Postagens aleatórias
federated_user_info: Esse perfil é de um servidor federado e pode estar
incompleto.
go_to_original_instance: Visualizar na instância remota
empty: Vazio
subscribe: Subscrever
unsubscribe: Cancelar subscrição
follow: Seguir
unfollow: Deixar de seguir
reply: Resposta
login_or_email: Login ou email
password: Password
dont_have_account: Não tem uma conta?
you_cant_login: Esqueceu-se da palavra-passe?
already_have_account: Já tem uma conta?
register: Registar
reset_password: Redefinir password
show_more: Ver mais
to: para
in: em
email: Email
repeat_password: Repetir password
terms: Termos do serviço
privacy_policy: Política de Privacidade
about_instance: Sobre
all_magazines: Todas as magazines
stats: Estatísticas
fediverse: Fediverse
create_new_magazine: Criar uma nova magazine
add_new_article: Adicionar um novo tópico
add_new_link: Adicionar um novo link
add_new_photo: Adicionar uma nova foto
add_new_post: Adicionar uma nova postagem
add_new_video: Adicionar um novo video
contact: Contato
faq: FAQ
rss: RSS
change_theme: Alterar tema
useful: Útil
help: Ajuda
reset_check_email_desc2: Se não receber um email, verifique a pasta de spam.
try_again: Tente de novo
up_vote: Upvoto
down_vote: Downvoto
email_confirm_header: Olá! Confirme o seu endereço de email.
email_confirm_content: 'Pronto para ativar a sua conta Mbin? Clique no link abaixo:'
email_verify: Confirme endereço de email
email_confirm_title: Confirme o seu endereço de email.
select_magazine: Selecione uma magazine
go_to_search: Ir para a procura
subscribed: Subscrito
all: Tudo
logout: Terminar sessão
classic_view: Vista clássica
compact_view: Vista compacta
chat_view: Vista de chat
tree_view: Vista de árvore
cards_view: Vista de cartão
3h: 3h
6h: 6h
1d: 1d
1m: 1m
links: Links
articles: Tópicos
1w: 1sem
photos: Fotos
videos: Videos
report: Reportar
share: Partilhar
copy_url_to_fediverse: Copiar URL original
share_on_fediverse: Partilhar no Fediverso
edit: Editar
are_you_sure: Tem a certeza?
moderate: Moderar
reason: Motivo
delete: Apagar
edit_post: Editar postagem
edit_comment: Gravar alterações
settings: Definições
general: Geral
profile: Perfil
blocked: Bloqueado
reports: Relatórios
notifications: Notificações
messages: Mensagens
homepage: Página principal
hide_adult: Esconder conteúdo NSFW
featured_magazines: Magazines em destaque
privacy: Privacidade
show_profile_followings: Mostrar utilizadores seguidos
notify_on_new_entry_reply: Qualquer nível de comentários nos tópicos da minha
autoria
notify_on_new_post_reply: Qualquer nível de resposta a publicações da minha
autoria
notify_on_new_post_comment_reply: Respostas aos meus comentários em qualquer
publicação
notify_on_new_entry: Novos tópicos (ligações ou artigos) em qualquer revista da
qual seja assinante
save: Guardar
about: Sobre
old_email: Email atual
new_email: Novo email
new_email_repeat: Confirmar o novo email
current_password: Password atual
new_password: Nova password
new_password_repeat: Confirmar nova password
change_email: Alterar email
change_password: Alterar password
expand: Expandir
collapse: Colapsar
domains: Domínios
error: Erro
votes: Votos
dark: Escuro
light: Claro
solarized_light: Claro Solarizado
solarized_dark: Escuro Solarizado
font_size: Tamanho da letra
size: Tamanho
boosts: Upvotos
yes: Sim
no: Não
show_magazines_icons: Mostrar os ícones das magazines
show_thumbnails: Mostrar miniaturas
rounded_edges: Cantos redondos
removed_thread_by: removeu um tópico de
removed_comment_by: removeu um comentário de
restored_comment_by: restaurou comentário por
removed_post_by: removeu um post de
restored_post_by: restaurou uma publicação de
he_banned: banido
he_unbanned: desbanidos
read_all: Ler tudo
show_all: Mostrar tudo
added_new_thread: Adicionado um novo tópico
edited_thread: Editado um tópico
mod_remove_your_thread: Um moderador removeu o seu tópico
added_new_comment: Adicionado um novo comentário
edited_comment: Editado um comentário
replied_to_your_comment: Respondeu ao seu comentário
mod_deleted_your_comment: Um moderador apagou o seu comentário
added_new_post: Adicionado uma nova postagem
mod_remove_your_post: Um moderador removeu a sua postagem
added_new_reply: Adicionada uma nova resposta
wrote_message: Escreva uma mensagem
banned: Baniu-o
removed: Removido por um moderador
mentioned_you: Mencionou-o
comment: Comentário
post: Postagem
ban_expired: O ban expirou
purge: Limpar
send_message: Enviar mensagem direta
message: Mensagem
infinite_scroll: Rolagem infinita
show_top_bar: Mostrar barra do topo
subject_reported: O conteúdo foi reportado.
sidebar_position: Posição da barra lateral
left: Esquerda
right: Direita
federation: Federação
status: Estado
on: Ligado
instances: Instâncias
upload_file: Carregar ficheiro
from_url: Do url
magazine_panel: Painel da magazine
reject: Rejeitar
approve: Aprovar
ban: Banir
filters: Filtros
approved: Aprovado
add_moderator: Adicionar moderador
add_badge: Adicionar distintivo
bans: Expulsões
created: Criado
expires: Expira
perm: Permanente
expired_at: Expirou em
add_ban: Adicionar expulsão
trash: Lixo
icon: Ícone
pin: Fixar
unpin: Desafixar
change_magazine: Alterar magazine
change_language: Alterar idioma
change: Alterar
pinned: Fixado
preview: Previsualizar
article: Tópico
reputation: Reputação
note: Nota
users: Utilizadores
content: Conteúdo
week: Semana
weeks: Semanas
month: Mês
months: Meses
year: Ano
federated: Federado
local: Local
dashboard: Painel
contact_email: Email de contato
meta: Meta
instance: Instância
pages: Páginas
FAQ: FAQ
type_search_term: Introduza termo de pesquisa
federation_enabled: Federação ativada
registrations_enabled: Registo ativado
registration_disabled: Registos desativados
restore: Restaurar
type.smart_contract: Contrato inteligente
active: Ativo
agree_terms: Consentimento dos %terms_link_start%Termos e
Condições%terms_link_end% e %policy_link_start%Política de
Privacidade%policy_link_end%
check_email: Verifique o seu email
comments_count: '{0}Comentários|{1}Comentário|]1,Inf[ Comentários'
reset_check_email_desc: Se já houver uma conta associada ao seu endereço de
e-mail, deverá receber um e-mail em breve contendo uma ligação que poderá ser
usada para redefinir a sua palavra-passe. Esta ligação expirará em %expire%.
replies: Respostas
markdown_howto: Como funciona o editor?
email_confirm_expire: O link expira em uma hora.
federated_magazine_info: Esta revista é de um servidor federado e pode estar
incompleta.
remember_me: Lembrar-me
username: Username
table_view: Vista de tabela
edited_post: Editou uma publicação
12h: 12h
deleted: Apagado pelo autor
1y: 1a
mod_log_alert: AVISO - O Modlog pode conter conteúdo desagradável ou perturbador
que foi removido pelos moderadores. Por favor, tenha cuidado.
sticky_navbar: Barra de navegação fixa
copy_url: Copiar url de Mbin
off: Desligado
rejected: Rejeitado
appearance: Aparência
done: Feito
show_profile_subscriptions: Mostrar subscrições de magazines
writing: Escrita
notify_on_new_entry_comment_reply: Respostas aos meus comentários em qualquer
tópico
admin_panel: Painel de administração
notify_on_new_posts: Novas publicações de qualquer revista a que estou inscrito
theme: Tema
show_users_avatars: Mostrar os avatars dos utilizadores
restored_thread_by: restaurou um tópico de
add_new: Adicionar novo
url: URL
title: Título
body: Corpo
tags: Etiquetas
badges: Distintivos
is_adult: 18+ / NSFW
eng: ENG
oc: OC
image: Imagem
image_alt: Texto alternativo da imagem
name: Nome
description: Descrição
rules: Regras
domain: Domínio
followers: Seguidores
following: A seguir
subscriptions: Subscrições
overview: Visão geral
cards: Cartões
columns: Colunas
user: Utilizador
joined: Aderiu
moderated: Moderado
people_local: Local
people_federated: Federado
reputation_points: Pontos de reputação
related_tags: Etiquetas relacionadas
go_to_content: Ir para o conteúdo
go_to_filters: Ir para os filtros
flash_thread_new_success: O tópico foi criado com sucesso e agora está visível
para outros utilizadores.
flash_thread_edit_success: O tópico foi editado com sucesso.
flash_thread_delete_success: O tópico foi apagado com sucesso.
flash_thread_unpin_success: O tópico foi desafixado com sucesso.
flash_register_success: 'Bem-vindo a bordo! A sua conta já está registada. Uma última
etapa: verifique a sua caixa de entrada para receber uma ligação de ativação que
dará vida à sua conta.'
flash_thread_pin_success: O tópico foi fixado com sucesso.
flash_magazine_new_success: A revista foi criada com sucesso. Pode adicionar
novo conteúdo ou explorar o painel de administração da revista.
flash_magazine_edit_success: A revista foi editada com sucesso.
too_many_requests: Limite excedido, por favor tente novamente mais tarde.
set_magazines_bar: Barra de magazines
set_magazines_bar_desc: adicione os nomes dos magazines após a virgula
set_magazines_bar_empty_desc: se o espaço estiver vazio, magazines ativos serão
mostrados na barra
subscribe_for_updates: Inscreva-se para começar a receber atualizações.
remove_media: Remover mídia
unmark_as_adult: Desmarcar como NSFW
flash_mark_as_adult_success: A publicação foi marcada com sucesso como NSFW.
delete_account: Excluir conta
menu: Menu
flash_unmark_as_adult_success: A publicação foi desmarcada com sucesso como
NSFW.
unban_hashtag_btn: Desbanir Hashtag
unban_hashtag_description: Ao "desbanir" uma hashtag, novas publicações com essa
hashtag poderão ser criadas. As publicações existentes com essa hashtag não
serão mais ocultadas.
dynamic_lists: Listas dinâmicas
banned_instances: Instâncias banidas
report_issue: Relatar problema
disabled: Desativado
hidden: Oculto
enabled: Ativado
default_theme: Tema padrão
ban_hashtag_description: Ao banir uma hashtag, você impedirá a criação de
publicações com essa hashtag, além de ocultar as publicações existentes com
essa hashtag.
Your account is not active: Sua conta não está ativa.
firstname: Nome
reload_to_apply: Recarregar a página para aplicar as alterações
marked_for_deletion_at: Marcado para ser excluído em %date%
mark_as_adult: Marcar como NSFW
ban_account: Banir conta
header_logo: Logotipo do cabeçalho
filter.fields.only_names: Somente nomes
filter.fields.names_and_descriptions: Nomes e descrições
unban_account: Desbanir conta
sidebar: Barra lateral
captcha_enabled: Captcha ativado
browsing_one_thread: Você está navegando apenas em um tópico da discussão! Todos
os comentários estão disponíveis na página do post.
return: Retornar
filter.adult.hide: Ocultar NSFW
filter.adult.show: Mostrar NSFW
filter.adult.only: Somente NSFW
your_account_has_been_banned: Sua conta foi banida
send: Enviar
sticky_navbar_help: A barra de navegação permanecerá na parte superior da página
quando você rolar para baixo.
toolbar.italic: Itálico
kbin_promo_title: Crie sua própria instância
kbin_intro_title: Explorar o Fediverso
infinite_scroll_help: Carregar automaticamente mais conteúdo quando chegar ao
final da página.
subscribers_count: '{0}Inscritos|{1}Inscrito|]1,Inf[ Inscritos'
followers_count: '{0}Seguidores|{1}Seguidor|]1,Inf[ Seguidores'
marked_for_deletion: Marcado para ser excluído
sort_by: Ordenar por
filter_by_subscription: Filtrar por assinatura
filter_by_federation: Filtrar por status da federação
kbin_bot: Agente Mbin
toolbar.bold: Negrito
Your account has been banned: Sua conta foi banida.
active_users: Pessoas ativas
password_confirm_header: Confirme sua solicitação de alteração de senha.
toolbar.strikethrough: Riscado
Password is invalid: Senha inválida.
toolbar.header: Cabeçalho
oauth2.grant.domain.all: Assine ou bloqueie domínios e visualize os domínios que
você assinou ou bloqueou.
downvotes_mode: Modo de votos negativos
change_downvotes_mode: Alterar o modo de votos negativos
errors.server500.description: Desculpe, algo deu errado aqui do nosso lado. Se
você continuar a ver esse erro, tente entrar em contato com o proprietário da
instância. Se essa instância não estiver funcionando, verifique
%link_start%outras instâncias do Mbin%link_end% enquanto isso, até que o
problema seja resolvido.
errors.server404.title: 404 Não encontrado
errors.server403.title: 403 Proibido
email.delete.title: Pedido de exclusão da conta do usuário
block: Bloquear
always_disconnected_magazine_info: Esta revista não está recebendo atualizações.
from: de
resend_account_activation_email_success: Se houver uma conta associada a esse
e-mail, enviaremos um novo e-mail de ativação.
resend_account_activation_email_description: Digite o endereço de e-mail
associado à sua conta. Nós te enviaremos outro e-mail de ativação.
custom_css: CSS Personalizado
ignore_magazines_custom_css: Ignorar o CSS personalizado das revistas
oauth.consent.title: Formulário de Consentimento OAuth2
oauth.consent.app_requesting_permissions: gostaria de realizar as seguintes
ações em seu nome
oauth.consent.to_allow_access: Para permitir esse acesso, clique no botão
'Permitir' abaixo
oauth.consent.allow: Permitir
oauth.client_identifier.invalid: ID de cliente OAuth inválido!
oauth2.grant.moderate.magazine.ban.delete: Desbanir usuários em suas revistas
moderadas.
unblock: Desbloquear
private_instance: Forçar os usuários a fazer login antes de poderem acessar
qualquer conteúdo
oauth2.grant.moderate.magazine.list: Leia uma lista de suas revistas moderadas.
oauth2.grant.moderate.magazine.reports.all: Gerencie denúncias nas suas revistas
moderadas.
oauth2.grant.moderate.magazine_admin.all: Crie, edite ou exclua suas próprias
revistas.
oauth2.grant.moderate.magazine.reports.read: Leia as denúncias nas suas revistas
moderadas.
oauth2.grant.moderate.magazine.trash.read: Veja o conteúdo descartado em suas
revistas moderadas.
oauth2.grant.moderate.magazine_admin.create: Crie novas revistas.
oauth2.grant.moderate.magazine_admin.edit_theme: Edite o CSS personalizado das
suas revistas.
oauth2.grant.subscribe.general: Assine ou siga qualquer revista, domínio ou
usuário e veja as revistas, domínios e usuários nos quais você se inscreveu.
oauth2.grant.moderate.magazine_admin.update: Edite as regras, a descrição, o
status NSFW ou o ícone de qualquer uma das suas revistas.
oauth2.grant.domain.block: Bloqueie ou desbloqueie domínios e visualize os
domínios que você bloqueou.
email.delete.description: O usuário seguinte solicitou que sua conta fosse
excluída
resend_account_activation_email: Reenviar o e-mail de ativação da conta
errors.server429.title: 429 Solicitações em excesso
email_confirm_button_text: Confirme sua solicitação de alteração de senha
email_confirm_link_help: Como alternativa, você pode copiar e colar o seguinte
em seu navegador
oauth.consent.app_has_permissions: já pode executar as seguintes ações
oauth.client_not_granted_message_read_permission: Este aplicativo não recebeu
permissão para ler suas mensagens.
restrict_oauth_clients: Restringir a criação de clientes OAuth2 aos
administradores
oauth2.grant.moderate.magazine.reports.action: Aceite ou rejeite denúncias nas
suas revistas moderadas.
oauth2.grant.admin.all: Execute ações administrativas em sua instância.
oauth2.grant.read.general: Leia todo o conteúdo ao qual você tem acesso.
resend_account_activation_email_error: Houve um problema ao enviar esta
solicitação. Talvez não tenha uma conta associada a esse e-mail ou talvez ele
já esteja ativado.
oauth2.grant.domain.subscribe: Assine ou cancele a assinatura de domínios e
visualize os domínios a que você se inscreveu.
oauth2.grant.moderate.magazine_admin.delete: Exclua qualquer uma de suas
revistas.
oauth2.grant.block.general: Bloqueie ou desbloqueie qualquer revista, domínio ou
usuário e visualize as revistas, os domínios e os usuários que você bloqueou.
comment_not_found: Comentário não encontrado
oauth.consent.deny: Recusar
oauth2.grant.moderate.magazine_admin.moderators: Adicione ou remova moderadores
das suas revistas.
oauth2.grant.moderate.magazine_admin.badges: Crie ou remova selos das revistas
que possui.
disconnected_magazine_info: Esta revista não está recebendo atualizações (última
atividade %dias% dia(s) atrás).
resend_account_activation_email_question: Conta inativa?
oauth.consent.grant_permissions: Conceder permissões
account_banned: A conta foi banida.
flash_image_download_too_large_error: Não foi possível criar a imagem, pois ela
é muito grande (tamanho máximo %bytes%)
flash_comment_edit_error: Não foi possível editar o comentário. Algo deu errado.
flash_user_settings_general_success: As configurações do utilizador foram
gravadas com sucesso.
flash_user_settings_general_error: Não foi possível gravar as configurações do
utilizador.
flash_user_edit_profile_error: Não foi possível gravar as configurações de
perfil.
announcement: Anúncio
magazine_log_entry_unpinned: a entrada fixada foi removida
manually_approves_followers: Aprovar manualmente os seguidores
user_verify: Ativar conta
random_entries: Tópicos aleatórios
random_magazines: Revistas aleatórias
oauth2.grant.user.block: Bloquear ou desbloquear utilizadores e ver a lista de
utilizadores bloqueados.
oauth2.grant.moderate.entry_comment.set_adult: Marcar comentários em tópicos
como NSFW nas suas revistas moderadas.
oauth2.grant.moderate.magazine.ban.read: Visualizar utilizadores banidos nas
suas revistas moderadas.
oauth2.grant.admin.instance.information.edit: Atualizar as páginas Sobre,
Perguntas frequentes, Contato, Termos de serviço e Política de privacidade da
sua instância.
magazine_theme_appearance_custom_css: CSS personalizado que será aplicado quando
visualizar o conteúdo da sua revista.
2fa.backup_codes.recommendation: Recomenda-se que mantenha uma cópia deles num
local seguro.
subscription_sidebar_pop_out_left: Mover para a barra lateral separada à
esquerda
subscription_sidebar_pop_in: Mover as assinaturas para o painel em linha
flash_comment_new_success: O comentário foi criado com sucesso.
page_width_fixed: Fixo
deletion: Apagar
direct_message: Mensagem direta
tag: Tag
unban: Desbanir
ban_hashtag_btn: Banir Hashtag
auto_preview: Visualização automática de mídia
oauth2.grant.user.notification.all: Leia e limpe as suas notificações.
oauth2.grant.moderate.magazine.ban.all: Gerir utilizadores banidos nas suas
revistas moderadas.
oauth2.grant.moderate.magazine.ban.create: Banir utilizadores nas suas revistas
moderadas.
oauth2.grant.admin.entry_comment.purge: Apagar completamente um comentário em
tópicos da sua instância.
2fa.qr_code_img.alt: Um código QR que permite a configuração da autenticação de
dois fatores para a sua conta
2fa.qr_code_link.title: Ao aceder esta ligação, permite que a sua plataforma
registe esta autenticação de dois fatores
2fa.user_active_tfa.title: O utilizador tem a 2FA ativada
2fa.backup_codes.help: Pode usar estes códigos quando não tiver o seu
dispositivo ou aplicação de autenticação de dois fatores. Não os verá
novamente e poderá usar cada um deles apenas uma
vez .
magazine_log_mod_removed: removeu um moderador
edit_entry: Editar tópico
default_theme_auto: Claro/escuro (Detecção Automática)
preferred_languages: Filtrar idiomas dos tópicos e postagens
oauth2.grant.user.oauth_clients.read: Leia as permissões que concedeu a outras
aplicações do OAuth2.
oauth2.grant.moderate.post.trash: Eliminar ou restaurar publicações nas suas
revistas moderadas.
2fa.authentication_code.label: Código de Autenticação
2fa.backup-create.help: Pode criar códigos de autenticação de backup; ao fazer
isso, os códigos existentes serão invalidados.
flash_account_settings_changed: As configurações da sua conta foram alteradas
com sucesso. Precisará fazer login novamente.
subscription_sort: Ordenar
pending: Pendente
deleted_by_moderator: O tópico, a publicação ou o comentário foi apagado pelo
moderador
register_push_notifications_button: Cadastre-se para notificações push
tokyo_night: Noite em Tóquio
federation_page_enabled: Página da federação ativada
oauth2.grant.magazine.subscribe: Assine ou cancele a assinatura de revistas e
veja as revistas que assinou.
oauth2.grant.post.vote: Dê um voto positivo, negativo ou um impulso numa
publicação.
oauth2.grant.user.message.all: Leia as suas mensagens e envie mensagens para
outros utilizadores.
oauth2.grant.user.message.read: Leia as suas mensagens.
delete_content_desc: Apagar o conteúdo do utilizador, deixando as respostas de
outros utilizadores nos tópicos, publicações e comentários criados.
action: Ação
solarized_auto: Solarizado (Detecção Automática)
kbin_promo_desc: '%link_start%Clone o repositório%link_end% e desenvolva o fediverso'
filter.origin.label: Escolha a origem
filter.fields.label: Escolha os campos que deseja pesquisar
filter.adult.label: Escolha se deseja exibir NSFW
your_account_is_not_active: A sua conta não foi ativada. Verifique o seu e-mail
para obter instruções de ativação da conta ousolicite
um novo e-mail de ativação da conta.
toolbar.quote: Citação
toolbar.code: Código
toolbar.link: Ligação
toolbar.image: Imagem
oauth2.grant.entry.create: Crie novos tópicos.
oauth2.grant.post.all: Crie, edite ou apague os seus microblogs e vote,
impulsione ou denuncie qualquer microblog.
moderation.report.approve_report_title: Aprovar a Denúncia
moderation.report.reject_report_title: Rejeitar a Denúncia
moderation.report.reject_report_confirmation: Tem certeza de que deseja rejeitar
essa denúncia?
show_subscriptions: Mostrar assinaturas
flash_post_new_success: A publicação foi criada com sucesso.
purge_magazine: Purgar revista
suspend_account: Suspender conta
unsuspend_account: Cancelar a suspensão da conta
accept: Aceitar
sso_registrations_enabled.error: Novos registos de conta com gestores de
identidade de terceiros estão desativados no momento.
sso_only_mode: Restringir o login e o registo apenas aos métodos de SSO
related_entry: Relacionado
continue_with: Continue com
two_factor_authentication: Autenticação de dois fatores
oauth2.grant.moderate.magazine_admin.tags: Crie ou remova tags das revistas que
possui.
oauth2.grant.entry.all: Crie, edite ou apague os seus tópicos e vote, impulsione
ou denuncie qualquer um deles.
oauth2.grant.entry_comment.edit: Edite os seus comentários existentes nos
tópicos.
oauth2.grant.post.delete: Apague as suas publicações existentes.
oauth2.grant.user.profile.all: Leia e edite o seu perfil.
2fa.verify_authentication_code.label: Inserir um código de dois fatores para
verificar a configuração
password_and_2fa: Palavra-passe e 2FA
subscriptions_in_own_sidebar: Numa barra lateral separada
sidebars_same_side: Barras laterais no mesmo lado
subscription_sidebar_pop_out_right: Mover para a barra lateral separada à
direita
subscription_panel_large: Painel grande
restore_magazine: Restaurar revista
account_unsuspended: A suspensão da conta foi cancelada.
account_unbanned: A conta foi desbanida.
account_is_suspended: A conta do utilizador está suspensa.
remove_following: Remover seguidor
apply_for_moderator: Candidatar-se a moderador
abandoned: Abandonado
ownership_requests: Solicitações de propriedade
related_magazines: Revistas relacionadas
boost: Dar Boost
mercure_enabled: Mercure ativado
errors.server500.title: 500 Erro interno do servidor
oauth2.grant.vote.general: Pode votar a favor, contra ou impulsionar tópicos,
postagens ou comentários.
oauth2.grant.entry.report: Denunciar qualquer tópico.
oauth2.grant.entry_comment.delete: Apague os seus comentários existentes nos
tópicos.
oauth2.grant.entry_comment.vote: Dê um voto positivo, negativo ou impulsione
qualquer comentário num tópico.
oauth2.grant.post.create: Criar publicações.
oauth2.grant.post_comment.delete: Apague os seus comentários existentes em
publicações.
oauth2.grant.moderate.post_comment.all: Moderar comentários nas publicações das
suas revistas moderadas.
oauth2.grant.admin.instance.stats: Veja as estatísticas da sua instância.
flash_post_unpin_success: A publicação foi desafixada com sucesso.
oauth2.grant.moderate.post.pin: Fixe as publicações na parte superior das suas
revistas moderadas.
delete_content: Apagar conteúdo
purge_content_desc: Apagar completamente o conteúdo do utilizador, incluindo
apagar as respostas de outros utilizadores em tópicos, postagens e comentários
criados.
2fa.verify: Verificar
2fa.code_invalid: O código de autenticação não é válido
flash_email_was_sent: O email foi enviado com sucesso.
flash_user_edit_password_error: Não foi possível alterar a palavra-passe.
change_my_avatar: Modificar o meu avatar
change_my_cover: Modificar a minha capa
oauth2.grant.admin.entry.purge: Apagar completamente qualquer tópico da sua
instância.
oauth2.grant.entry_comment.report: Denunciar comentário num tópico.
oauth2.grant.magazine.all: Assine ou bloqueie revistas e veja as revistas
assinadas ou bloqueadas.
oauth2.grant.post.edit: Edite as suas publicações existentes.
oauth2.grant.post.report: Denuncie qualquer publicação.
oauth2.grant.admin.user.ban: Banir ou desbanir utilizadores da sua instância.
oauth2.grant.admin.instance.all: Visualizar e atualizar as configurações ou
informações da instância.
oauth2.grant.admin.instance.settings.all: Exibir ou atualizar as configurações
da sua instância.
oauth2.grant.admin.oauth_clients.all: Visualizar ou revogar clientes OAuth2 que
existem na sua instância.
request_magazine_ownership: Solicitar a propriedade da revista
hide: Ocultar
sensitive_warning: Conteúdo sensível
show: Exibir
oauth2.grant.write.general: Pode criar ou editar qualquer um dos seus tópicos,
postagens ou comentários.
oauth2.grant.delete.general: Apague qualquer um dos seus tópicos, postagens ou
comentários.
oauth2.grant.report.general: Denunciar tópicos, postagens ou comentários.
oauth2.grant.moderate.entry.trash: Jogar fora ou restaurar tópicos nas suas
revistas moderadas.
report_accepted: Uma denúncia foi aceita
kbin_intro_desc: é uma plataforma descentralizada para agregação de conteúdo e
microblogging que opera dentro da rede Fediverso.
auto_preview_help: Mostre as visualizações de mídia (foto, vídeo) em um tamanho
maior abaixo do conteúdo.
oauth2.grant.entry_comment.create: Criar comentários nos tópicos.
bot_body_content: "Bem-vindo ao Agente Mbin! Este Agente desempenha um papel crucial
na implementação do ActivityPub na Mbin. Ele garante que a Mbin possa comunicar
e se federar com outras instâncias no fediverso.\n\nO ActivityPub é um protocolo
de padrão aberto que permite que plataformas descentralizadas de redes sociais comuniquem
e interajam entre si. Ele permite que utilizadores em diferentes instâncias (servidores)
sigam, interajam e partilhem conteúdo na rede social federada conhecida como fediverso.
Ele fornece uma maneira padronizada para que os utilizadores publiquem conteúdo,
sigam outros utilizadores e participem de interações sociais, como curtir, partilhar
e comentar em tópicos ou postagens."
toolbar.spoiler: Spoiler
federated_search_only_loggedin: Pesquisa federada limitada se não estiver logado
account_deletion_title: Apagar contas
account_deletion_immediate: Apagar imediatamente
more_from_domain: Mais do domínio
oauth2.grant.entry.vote: Vote a favor, contra ou dê um impulso em qualquer
tópico.
oauth2.grant.entry_comment.all: Crie, edite ou apague os seus comentários nos
tópicos e vote, impulsione ou denuncie quaisquer comentários num tópico.
oauth2.grant.post_comment.all: Crie, edite ou apague os seus comentários nas
publicações e vote, impulsione ou denuncie qualquer comentário numa
publicação.
oauth2.grant.post_comment.create: Crie novos comentários nas publicações.
oauth2.grant.post_comment.report: Denuncie qualquer comentário numa publicação.
oauth2.grant.user.profile.read: Leia o seu perfil.
oauth2.grant.user.profile.edit: Edite o seu perfil.
oauth2.grant.user.notification.read: Leia as suas notificações, inclusive as de
mensagens.
oauth2.grant.user.notification.delete: Limpe as suas notificações.
oauth2.grant.moderate.entry.all: Moderar tópicos nas suas revistas moderadas.
oauth2.grant.moderate.entry.change_language: Altere o idioma dos tópicos nas
suas revistas moderadas.
oauth2.grant.moderate.entry.pin: Fixar os tópicos na parte superior das revistas
moderadas.
oauth2.grant.moderate.entry_comment.change_language: Altere o idioma dos
comentários nos tópicos das suas revistas moderadas.
oauth2.grant.user.oauth_clients.all: Leia e edite as permissões que concedeu a
outras aplicatções do OAuth2.
oauth2.grant.user.oauth_clients.edit: Edite as permissões que concedeu a outras
aplicações do OAuth2.
oauth2.grant.moderate.all: Execute ações de moderação para as quais tem
permissão nas suas revistas moderadas.
oauth2.grant.moderate.entry_comment.trash: Eliminar ou restaurar comentários em
tópicos das suas revistas moderadas.
oauth2.grant.moderate.post.all: Moderar as publicações nas suas revistas
moderadas.
oauth2.grant.moderate.post.change_language: Altere o idioma das publicações das
suas revistas moderadas.
oauth2.grant.moderate.post.set_adult: Marcar as publicações como NSFW nas suas
revistas moderadas.
oauth2.grant.moderate.post_comment.change_language: Alterar o idioma dos
comentários das publicações nas suas revistas moderadas.
oauth2.grant.moderate.post_comment.set_adult: Marcar comentários em publicações
como NSFW nas suas revistas moderadas.
oauth2.grant.moderate.post_comment.trash: Remover ou restaurar comentários de
publicações nas suas revistas moderadas.
oauth2.grant.admin.post.purge: Apagar completamente qualquer publicação da sua
instância.
oauth2.grant.admin.post_comment.purge: Apagar completamente uma publicação da
sua instância.
oauth2.grant.admin.magazine.all: Mover tópicos entre revistas ou apagá-las
completamente da sua instância.
oauth2.grant.admin.user.verify: Verificar utilizadores da sua instância.
oauth2.grant.admin.user.delete: Apagar utilizadores da sua instância.
oauth2.grant.admin.oauth_clients.read: Veja os clientes OAuth2 que existem na
sua instância e as suas estatísticas de uso.
oauth2.grant.admin.oauth_clients.revoke: Revogar o acesso a clientes OAuth2 na
sua instância.
last_active: Última atividade
flash_post_pin_success: A publicação foi fixada com sucesso.
comment_reply_position_help: Exibir o formulário de resposta a comentários na
parte superior ou inferior da página. Quando a “rolagem infinita” estiver
ativada, a posição sempre aparecerá na parte superior.
show_avatars_on_comments: Mostrar avatares de comentários
show_avatars_on_comments_help: Exibir/ocultar avatares de utilizadores ao
visualizar comentários num único tópico ou postagem.
magazine_theme_appearance_background_image: Imagem de fundo personalizada que
será aplicada quando visualizar o conteúdo da sua revista.
subject_reported_exists: Este conteúdo já foi denunciado.
delete_account_desc: Apagar a conta, incluindo as respostas de outros
utilizadores em tópicos, postagens e comentários criados.
schedule_delete_account: Programar o apagar
schedule_delete_account_desc: Programar o apagar desta conta em 30 dias. Isto
ocultará o utilizador e o seu conteúdo, além de impedir que ele faça login.
remove_schedule_delete_account: Remover o apagar programado
remove_schedule_delete_account_desc: Remover o apagar programado. Todo o
conteúdo estará disponível novamente e o utilizador poderá fazer login.
two_factor_backup: Códigos de backup da autenticação de dois fatores
2fa.setup_error: Erro ao ativar a 2FA para a conta
2fa.enable: Configurar a autenticação de dois fatores
2fa.disable: Desativar a autenticação de dois fatores
2fa.backup-create.label: Criar códigos de autenticação de backup
2fa.add: Adicionar à minha conta
2fa.available_apps: Use uma aplicação de autenticação de dois fatores, como
%google_authenticator%, %aegis% (Android) ou %raivo% (iOS) para fazer a
leitura do código QR.
flash_thread_new_error: Não foi possível criar o tópico. Algo deu errado.
flash_thread_tag_banned_error: Não foi possível criar o tópico. O conteúdo não é
permitido.
flash_post_new_error: Não foi possível criar a publicação. Algo deu errado.
flash_magazine_theme_changed_success: Atualizou com sucesso a aparência da
revista.
flash_magazine_theme_changed_error: Não foi possível atualizar a aparência da
revista.
flash_comment_edit_success: O comentário foi atualizado com sucesso.
flash_comment_new_error: Não foi possível criar o comentário. Algo deu errado.
flash_user_edit_profile_success: As configurações do perfil do utilizador foram
gravadas com sucesso.
flash_post_edit_success: A publicação foi editada com sucesso.
page_width: Largura da página
edit_my_profile: Editar o meu perfil
keywords: Palavras-chave
deleted_by_author: O tópico, a publicação ou o comentário foi apagado pelo autor
sensitive_show: Clique para mostrar
details: Pormenores
spoiler: Spoiler
all_time: Todo o período
edited: editado
restrict_magazine_creation: Restringir a criação de revistas locais a
administradores e moderadores globais
magazine_log_mod_added: adicionou um moderador
last_updated: Última atualização
unregister_push_notifications_button: Remover registo de push
test_push_notifications_button: Teste as notificações por push
notification_title_removed_post: Uma publicação foi removida
notification_title_edited_post: Uma publicação foi editada
notification_title_new_report: Uma nova denúncia foi criada
version: Versão
last_successful_deliver: Última entrega bem-sucedida
last_successful_receive: Última receção bem-sucedida
last_failed_contact: Último contato que não deu certo
magazine_posting_restricted_to_mods: Restringir a criação de tópicos aos
moderadores
max_image_size: Tamanho máximo do ficheiro
federation_page_dead_title: Instâncias mortas
federation_page_dead_description: Instâncias em que não poderíamos entregar ao
menos 10 atividades seguidas e onde a última entrega bem sucedida e -recebida
foi a mais de uma semana atrás
open_url_to_fediverse: Abrir URL original
delete_magazine: Apagar revista
magazine_is_deleted: A revista foi apagada. Pode restaurá-la dentro de 30 dias.
account_suspended: A conta foi suspensa.
oauth2.grant.moderate.magazine.all: Gerir banimentos, denúncias e visualizar
elementos descartados nas suas revistas moderadas.
oauth2.grant.admin.magazine.move_entry: Mover tópicos entre revistas da sua
instância.
oauth2.grant.admin.user.all: Banir, verificar ou apagar completamente os
utilizadores da sua instância.
oauth2.grant.admin.magazine.purge: Apagar completamente revistas da sua
instância.
oauth2.grant.admin.user.purge: Apagar completamente utilizadores da sua
instância.
oauth2.grant.admin.instance.settings.read: Exibir as configurações da sua
instância.
update_comment: Atualizar comentário
magazine_theme_appearance_icon: Ícone personalizado para a revista.
moderation.report.ban_user_description: Deseja banir o utilizador (%username%)
que criou este conteúdo desta revista?
moderation.report.approve_report_confirmation: Tem certeza de que deseja aprovar
esta denúncia?
alphabetically: Por ordem alfabética
flash_email_failed_to_sent: O e-mail não pode ser enviado.
page_width_auto: Automático
filter_labels: Filtrar Etiquetas
auto: Automático
page_width_max: Max
account_settings_changed: As configurações da sua conta foram alteradas com
sucesso. Precisará fazer login novamente.
magazine_deletion: Apagar a revista
sensitive_hide: Clique para ocultar
oauth2.grant.user.message.create: Envie mensagens para outros utilizadores.
oauth2.grant.admin.instance.settings.edit: Atualizar as configurações da sua
instância.
oauth2.grant.admin.federation.all: Exibir e atualizar instâncias atualmente
desfederadas.
oauth2.grant.admin.federation.read: Exibir a lista das instâncias desfederadas.
oauth2.grant.admin.federation.update: Adicionar ou remover instâncias de ou para
a lista de instâncias desfederadas.
moderation.report.ban_user_title: Banir Utilizador
purge_content: Purgar conteúdo
2fa.remove: Remover 2FA
cancel: Cancelar
sensitive_toggle: Alternar a visibilidade de conteúdo sensível
flash_posting_restricted_error: Criar tópicos é restrito aos moderadores desta
revista e não é um deles
magazine_posting_restricted_to_mods_warning: Somente os moderadores podem criar
tópicos nesta revista
server_software: Software do servidor
purge_account: Purgar conta
close: Fechar
flash_thread_edit_error: Não foi possível editar o tópico. Algo deu errado.
oauth2.grant.moderate.magazine_admin.stats: Veja o conteúdo, vote e veja as
estatísticas das revistas que possui.
oauth2.grant.entry.edit: Edite os tópicos existentes.
oauth2.grant.entry.delete: Apague os tópicos existentes.
oauth2.grant.post_comment.edit: Edite os seus comentários existentes em
publicações.
oauth2.grant.post_comment.vote: Dê um voto positivo, negativo ou impulsione um
comentário numa publicação.
oauth2.grant.user.all: Leia e edite o seu perfil, mensagens ou notificações;
Leia e edite as permissões que concedeu a outras apps; siga ou bloqueie outros
utilizadores; visualize listas de utilizadores que segue ou bloqueia.
oauth2.grant.user.follow: Siga ou deixe de seguir utilizadores e veja uma lista
dos utilizadores que segue.
cancel_request: Cancelar pedido
user_suspend_desc: Suspender a sua conta oculta o seu conteúdo na instância, mas
não o remove permanentemente e pode restaurá-la a qualquer momento.
remove_subscriptions: Remover assinaturas
subscription_header: Revistas Assinadas
position_bottom: Inferior
position_top: Topo
and: e
show_related_magazines: Mostrar revistas aleatórias
show_related_entries: Mostrar tópicos aleatórios
show_related_posts: Mostrar publicações aleatórias
show_active_users: Mostrar utilizadores ativos
add_mentions_posts: Adicionar tags de menção em publicações
related_entries: Tópicos relacionados
local_and_federated: Local e federado
add_mentions_entries: Adicionar tags de menção nos tópicos
toolbar.unordered_list: Lista desordenada
toolbar.ordered_list: Lista ordenada
toolbar.mention: Menção
federation_page_allowed_description: Instâncias conhecidas com as quais
federamos
federation_page_disallowed_description: Instâncias com as quais não nos
federamos
account_deletion_description: A sua conta será apagada em 30 dias, a menos que
opte por apagar a conta imediatamente. Para restaurar a sua conta dentro de 30
dias, faça login com as mesmas credenciais de utilizador ou entre em contato
com um administrador.
oauth2.grant.magazine.block: Bloqueie ou desbloqueie revistas e veja as revistas
que bloqueou.
oauth2.grant.moderate.entry.set_adult: Marcar os tópicos como NSFW nas suas
revistas moderadas.
oauth2.grant.moderate.entry_comment.all: Moderar comentários em tópicos nas suas
revistas moderadas.
flash_user_edit_email_error: Não foi possível alterar o e-mail.
flash_post_edit_error: Não foi possível editar a publicação.
user_badge_admin: Administrador
sso_registrations_enabled: Registos SSO ativados
sso_show_first: Mostrar o SSO primeiro nas páginas de login e registo
reported_user: Utilizador denunciado
reported: denunciado
report_subject: Assunto
own_report_rejected: Uma denúncia foi rejeitada
own_report_accepted: A sua denúncia foi aceita
own_content_reported_accepted: Uma denúncia do seu conteúdo foi aceita.
cake_day: Dia do bolo
someone: Alguém
back: Anterior
test_push_message: Olá, mundo!
notification_title_new_comment: Novo comentário
notification_title_removed_comment: Um comentário foi removido
notification_title_edited_comment: Um comentário foi editado
notification_title_mention: Foi mencionado
notification_title_new_reply: Nova resposta
notification_title_new_thread: Novo tópico
notification_title_removed_thread: Um tópico foi removido
notification_title_edited_thread: Um tópico foi editado
notification_title_ban: Foi banido
notification_title_message: Nova mensagem direta
notification_title_new_post: Nova publicação
new_user_description: Este utilizador é novo (ativo há menos que %days% dias)
new_magazine_description: Esta revista é nova (ativa há menos que %days% dias)
admin_users_active: Ativos
admin_users_inactive: Inativos
admin_users_suspended: Suspensos
admin_users_banned: Banidos
remove_user_avatar: Remover avatar
remove_user_cover: Remover capa
crosspost: Postagem cruzada
notify_on_user_signup: Novas inscrições
ban_expires: Expira o banimento
banner: Banner
type_search_term_url_handle: Digite o termo de pesquisa, URL ou identificador.
magazine_panel_tags_info: Fornecer apenas se você quiser que o conteúdo do
fediverse seja incluído nesta revista com base em tags
viewing_one_signup_request: Você só está vendo um pedido de inscrição por
%username%
your_account_is_not_yet_approved: Sua conta ainda não foi aprovada. Lhe
enviaremos um e-mail assim que os administradores tiverem aceitado o seu
pedido de inscrição.
toolbar.emoji: Emoji
account_deletion_button: Apagar conta
oauth2.grant.user.bookmark: Adicionar e remover favoritos
oauth2.grant.user.bookmark.add: Adicionar aos favoritos
oauth2.grant.user.bookmark.remove: Remover dos favoritos
oauth2.grant.user.bookmark_list: Leia, edite e exclua suas listas de favoritos
oauth2.grant.user.bookmark_list.read: Leia suas listas de favoritos
oauth2.grant.user.bookmark_list.edit: Edite suas listas de favoritos
oauth2.grant.user.bookmark_list.delete: Excluir suas listas de favoritos
oauth2.grant.moderate.entry.lock: Fechar tópicos em suas revistas moderadas,
para que ninguém possa comentar sobre eles
oauth2.grant.moderate.post.lock: Fechar microblogs em suas revistas moderadas,
para que ninguém possa comentar sobre eles
single_settings: Único
comment_reply_position: Posição de resposta de comentário
magazine_theme_appearance_banner: Banner personalizado para a revista. Ele é
exibido acima de todos os tópicos e deve estar possuir um amplo aspect ratio
(5:1, ou 1500px * 300px).
2fa.backup: Seus códigos de backup de dois fatores
2fa.manual_code_hint: Se você não puder digitalizar o QR code, digite o segredo
manualmente
flash_thread_ref_image_not_found: A imagem referenciada por 'imageHash' não pôde
ser encontrada.
search_type_content: Tópicos + Microblogs
select_user: Escolha um usuário
new_users_need_approval: Novos usuários têm que ser aprovados por um
administrador antes que eles possam fazer login.
signup_requests: Pedidos de inscrição
application_text: Explique por que você gostaria de participar
signup_requests_header: Pedidos de inscrição
signup_requests_paragraph: Esses usuários gostariam de se juntar ao seu
servidor. Não podem entrar até aprovar o pedido de inscrição.
flash_application_info: Um administrador precisa aprovar sua conta antes de
poder fazer login. Você receberá um e-mail assim que o pedido de inscrição for
processado.
email_application_approved_title: Seu pedido de inscrição foi aprovado
email_application_approved_body: Seu pedido de inscrição foi aprovado pelo
administrador do servidor. Agora você pode fazer login no servidor em %siteName% .
email_application_rejected_title: Seu pedido de inscrição foi rejeitado
email_application_rejected_body: Obrigado pelo seu interesse, mas lamentamos
informar que o seu pedido de inscrição foi recusado.
email_application_pending: Sua conta requer aprovação do administrador antes de
poder fazer login.
email_verification_pending: Você tem que verificar seu endereço de e-mail antes
de fazer login.
show_magazine_domains: Mostrar domínios de revistas
show_user_domains: Mostrar domínios de usuário
answered: respondidas
by: por
front_default_sort: Tipo padrão da página inicial
comment_default_sort: Tipo padrão de comentário
open_signup_request: Pedidos de inscrição abertos
image_lightbox_in_list: Thumbnails de tópicos abrem em tela cheia
compact_view_help: Uma visão compacta com menos margens, onde a mídia é movida
para o lado direito.
show_users_avatars_help: Exibir a imagem de avatar do usuário.
show_magazines_icons_help: Exibir o ícone da revista.
show_thumbnails_help: Mostre as imagens de thumbnails.
image_lightbox_in_list_help: Quando marcado, clicando no thumbnail mostra uma
janela de caixa de imagem modal. Quando desmarcado, clicar na miniatura abrirá
o tópico.
show_new_icons: Mostrar novos ícones
show_new_icons_help: Mostrar ícone para nova revista / usuário (mais ou menos 30
dias)
magazine_instance_defederated_info: A instância desta revista não é federada. A
revista, portanto, não receberá atualizações.
user_instance_defederated_info: A instância deste usuário não é federada.
flash_thread_instance_banned: A instância desta revista está banida.
show_rich_mention: Menções ricas
show_rich_mention_help: Renderizar um componente de usuário quando um usuário
for mencionado. Isso incluirá seu nome de exibição e foto de perfil.
show_rich_mention_magazine: Quantidade de menções em revistas
show_rich_mention_magazine_help: Renderize um componente de revista quando uma
revista for mencionada. Isso incluirá seu nome de exibição e ícone.
show_rich_ap_link: Quantidade de AP links
show_rich_ap_link_help: Renderizar um componente embutido quando outro conteúdo
do ActivityPub for vinculado.
attitude: Atitude
type_search_magazine: Limitar a busca à revista...
type_search_user: Limitar a busca ao autor...
modlog_type_entry_deleted: Tópico apagado
modlog_type_entry_restored: Tópico restaurado
modlog_type_entry_comment_deleted: Comentário do tópico apagado
modlog_type_entry_comment_restored: Comentário de tópico restaurado
modlog_type_entry_pinned: Tópico fixado
modlog_type_entry_unpinned: Tópico desfixado
modlog_type_post_deleted: Microblog apagado
modlog_type_post_restored: Microblog restaurado
modlog_type_post_comment_deleted: Resposta do microblog apagada
modlog_type_post_comment_restored: Resposta do microblog restaurada
modlog_type_ban: Usuário banido da revista
modlog_type_moderator_add: Moderador de revista adicionado
modlog_type_moderator_remove: Moderador de revista removido
everyone: Todo mundo
nobody: Ninguém
followers_only: Apenas seguidores
direct_message_setting_label: Quem pode enviar uma mensagem direta
delete_magazine_icon: Excluir ícone da revista
flash_magazine_theme_icon_detached_success: Ícone da revista excluído com
sucesso
delete_magazine_banner: Excluir banner de revista
flash_magazine_theme_banner_detached_success: Banner de revista excluído com
sucesso
federation_uses_allowlist: Use a lista de permissões para federação
defederating_instance: Desfederar instância %i
their_user_follows: Quantidade de usuários de sua instância seguindo usuários em
nossa instância
our_user_follows: Quantidade de usuários de nossa instância seguindo usuários em
sua instância
their_magazine_subscriptions: Quantidade de usuários de sua instância inscritos
em revistas em nossa instância
our_magazine_subscriptions: Quantidade de usuários em nossa instância inscritos
em revistas de sua instância
confirm_defederation: Confirmar desfederação
flash_error_defederation_must_confirm: Você tem que confirmar a desfederação
allowed_instances: Instâncias permitidas
btn_deny: Recusar
btn_allow: Permitir
ban_instance: Banir instância
allow_instance: Permitir instância
federation_page_use_allowlist_help: Se uma lista de permissão for usada, essa
instância somente federará com as instâncias explicitamente permitidas. Caso
contrário, esta instância vai federar com cada instância, exceto aquelas que
estão banidas.
you_have_been_banned_from_magazine: Você foi banido da revista %m.
you_have_been_banned_from_magazine_permanently: Você foi permanentemente banido
da revista %m.
you_are_no_longer_banned_from_magazine: Você não está mais banido da revista %m.
front_default_content: Visão padrão da página inicial
default_content_default: Predefinição do servidor (Tópicos)
default_content_combined: Tópicos + Microblog
default_content_threads: Tópicos
default_content_microblog: Microblog
combined: Combinado
sidebar_sections_random_local_only: Restringir seções da barra lateral "Tópicos
aleatórios/Postagens" para apenas local
sidebar_sections_users_local_only: Restringir seção de barra lateral "pessoas
ativas" para apenas local
random_local_only_performance_warning: Habilitar "Aleatoriedade apenas local"
pode causar impacto de desempenho SQL.
discoverable: Descobrível
user_discoverable_help: Se isso estiver ativado, seu perfil, tópicos, microblogs
e comentários podem ser encontrados através da pesquisa e pelos painéis
aleatórios. Seu perfil também pode aparecer no painel de usuário ativo e na
página de pessoas. Se for desativado, seus posts ainda serão visíveis para
outros usuários, mas eles não aparecerão por todo feed.
magazine_discoverable_help: Se isso estiver ativado, esta revista e tópicos,
microblogs e comentários desta revista podem ser encontrados através da
pesquisa e por painéis aleatórios. Se isso for desativado, a revista ainda
aparecerá na lista de revistas, mas os tópicos e microblogs não aparecerão em
todo o feed.
flash_thread_lock_success: Tópico fechado com sucesso
flash_thread_unlock_success: Tópico reaberto com sucesso
flash_post_lock_success: Microblog fechado com sucesso
flash_post_unlock_success: Microblog reaberto com sucesso
lock: Fechar
unlock: Reabrir
comments_locked: Fechado para comentários.
magazine_log_entry_locked: fechar comentários de
magazine_log_entry_unlocked: reabrir comentários de
modlog_type_entry_lock: Tópico fechado
modlog_type_entry_unlock: Tópico reaberto
modlog_type_post_lock: Microblog fechado
modlog_type_post_unlock: Microblog reaberto
contentnotification.muted: Mutar | não receber notificações
contentnotification.default: Padrão | obter notificações de acordo com suas
configurações padrão
contentnotification.loud: Geral | obter todas as notificações
indexable_by_search_engines: Indexável por motores de busca
user_indexable_by_search_engines_help: Se esta configuração for falsa, os
motores de busca são aconselhados a não indexar qualquer um dos seus tópicos e
microblogs, no entanto, seus comentários não são afetados por este e maus
atores podem ignorá-lo. Esta configuração também é federada para outros
servidores.
magazine_indexable_by_search_engines_help: Se esta configuração for falsa, os
motores de busca são aconselhados a não indexar nenhum dos tópicos e
microblogs nestas revistas. Isso inclui a landing page e todas as páginas de
comentários. Esta configuração também é federada para outros servidores.
search_type_entry: Tópicos
================================================
FILE: translations/messages.pt_BR.yaml
================================================
type.link: Link
type.article: Fio
type.photo: Foto
type.video: Vídeo
type.smart_contract: Contrato inteligente
type.magazine: Magazine
thread: Fio
threads: Fios
microblog: Microblog
people: Pessoas
events: Eventos
magazine: Magazine
search: Buscar
add: Adicionar
commented: Comentado
change_view: Alterar visualização
filter_by_time: Filtrar por tempo
filter_by_type: Filtrar por tipo
filter_by_subscription: Filtrar por inscrição
magazines: Magazines
select_channel: Selecionar um canal
sort_by: Ordenar por
login: Fazer login
oldest: Mais velho
top: Topo
hot: Popular
active: Ativo
newest: Mais novo
filter_by_federation: Filtrar por status de federação
marked_for_deletion: Marcado para exclusão
marked_for_deletion_at: Marcado para exclusão em %date%
favourites: Votos a favor
favourite: Favorito
more: Mais
avatar: Avatar
added: Adicionou
no_comments: Sem comentários
created_at: Criado
owner: Dono
subscribers: Inscritos
online: Online
comments: Comentários
posts: Postagens
replies: Respostas
moderators: Moderadores
mod_log: Registro de moderação
add_comment: Adicionar comentário
add_post: Adicionar postagem
contact: Contato
faq: Perguntas Freqüentes (FAQ)
delete: Excluir
comments_count: '{0}Comentários|{1}Comentário|]1,Inf[ Comentários'
subscribers_count: '{0}Inscritos|{1}Inscrito|]1,Inf[ Inscritos'
followers_count: '{0}Seguidores|{1}Seguidor|]1,Inf[ Seguidores'
add_media: Adicionar mídia
remove_media: Remover mídia
markdown_howto: Como funciona o editor?
enter_your_comment: Insira seu comentário
activity: Atividade
cover: Capa
subscribe_for_updates: Inscreva-se para começar a receber atualizações.
federated_user_info: Este perfil é de um servidor federado e pode estar
incompleto.
empty: Vazio
go_to_original_instance: Ver na instância remota
subscribe: Se inscrever
follow: Seguir
unfollow: Deixar de seguir
unsubscribe: Cancelar inscrição
remember_me: Lembrar de mim
reply: Responder
login_or_email: Login ou e-mail
password: Senha
dont_have_account: Não possui uma conta?
you_cant_login: Esqueceu a sua senha?
show_more: Mostrar mais
to: para
in: em
already_have_account: Você já possui uma conta?
reset_password: Redefinir a senha
username: Nome do usuário
email: E-mail
repeat_password: Repetir a senha
privacy_policy: Política de privacidade
useful: Útil
help: Ajuda
check_email: Verifique o seu e-mail
email_confirm_content: 'Pronto para ativar a sua conta Mbin? Clique no link abaixo:'
email_verify: Confirmar endereço de e-mail
email_confirm_expire: O link expirará em uma hora.
email_confirm_title: Confirme seu endereço de e-mail.
add_new: Adicionar novo(a)
overview: Visão Geral
new_email_repeat: Confirme novo e-mail
current_password: Senha atual
new_password: Nova senha
new_password_repeat: Confirmar nova senha
change_email: Alterar e-mail
change_password: Alterar a senha
expand: Expandir
domains: Domínios
flash_register_success: 'Bem-vindo a bordo! Sua conta já está registrada. Uma última
etapa: verifique sua caixa de entrada para receber um link de ativação que dará
vida à sua conta.'
send: Enviar
firstname: Nome
unban_account: Desbanir conta
banned_instances: Instâncias banidas
kbin_intro_title: Explore o Fediverso
kbin_promo_title: Crie sua própria instância
return: Retornar
reload_to_apply: Recarregar a página para aplicar alterações
filter.origin.label: Escolha a origem
filter.adult.hide: Ocultar NSFW
filter.adult.show: Mostrar NSFW
filter.adult.only: Somente NSFW
password_confirm_header: Confirme seu pedido de alteração de senha.
disabled: Desativado
hidden: Oculto
enabled: Ativado
reset_check_email_desc2: Se você não receber um e-mail, verifique sua pasta de
spam.
url: URL
title: Título
reset_check_email_desc: Se já houver uma conta associada ao seu endereço de
e-mail, você deverá receber um e-mail em breve contendo um link que poderá ser
usado para redefinir sua senha. Esse link expirará em %expire%.
body: Conteúdo
rules: Regras
domain: Domínio
followers: Seguidores
compact_view: Vista compacta
chat_view: Vista do chat
links: Links
photos: Fotos
1m: 1m
general: Geral
about: Sobre
old_email: E-mail atual
solarized_light: Luz Solarizada
solarized_dark: Escuro Solarizado
size: Tamanho
type_search_term: Digite o termo de pesquisa
auto_preview: Visualização automática de mídia
dynamic_lists: Listas dinâmicas
captcha_enabled: Captcha habilitado
from: de
about_instance: Sobre
stats: Estatísticas
fediverse: Fediverso
add_new_link: Adicionar novo link
add_new_photo: Adicionar nova foto
add_new_video: Adicionar novo vídeo
rss: RSS
change_theme: Mudar tema
try_again: Tente novamente
down_vote: Reduzir
is_adult: 18+ / NSFW
email_confirm_header: Olá! Confirme seu endereço de e-mail.
image_alt: Texto alternativo da imagem
description: Descrição
image: Imagem
name: Nome
following: Seguindo
columns: Colunas
user: Usuário
people_federated: Federado
go_to_content: Ir para o conteúdo
logout: Sair
classic_view: Vista clássica
go_to_filters: Ir para os filtros
subscribed: Inscrito
all: Todos
3h: 3h
12h: 12h
1d: 1d
6h: 6h
1w: 1s
1y: 1a
videos: Vídeos
share: Compartilhar
copy_url: Copiar URL Mbin
copy_url_to_fediverse: Copiar URL original
share_on_fediverse: Compartilhar no Fediverso
edit: Editar
are_you_sure: Tem certeza?
moderate: Moderar
reason: Razão
menu: Menu
profile: Perfil
blocked: Bloqueado
reports: Denúncias
notifications: Notificações
messages: Mensagens
appearance: Aparência
homepage: Página inicial
hide_adult: Ocultar conteúdo NSFW
privacy: Privacidade
save: Salvar
error: Erro
new_email: Novo e-mail
theme: Tema
dark: Escuro
light: Claro
default_theme: Tema padrão
solarized_auto: Solarizado (Detecção Automática)
default_theme_auto: Claro/escuro (Detecção Automática)
font_size: Tamanho da fonte
show_users_avatars: Mostrar avatares dos usuários
yes: Sim
no: Não
show_thumbnails: Mostrar miniaturas
rounded_edges: Bordas arredondadas
read_all: Ler tudo
show_all: Mostrar tudo
FAQ: Perguntas Frequentes (FAQ)
restore: Restaurar
settings: Configurações
federation_enabled: Federação habilitada
registration_disabled: Registro desativado
registrations_enabled: Registro ativado
Password is invalid: A senha é inválida.
Your account is not active: Sua conta não está ativa.
Your account has been banned: Sua conta foi banida.
delete_account: Excluir conta
purge_account: Purgar conta
ban_account: Banir conta
sidebar: Barra lateral
sticky_navbar_help: A barra de navegação permanecerá na parte superior da página
quando você rolar para baixo.
auto_preview_help: Expanda automaticamente as pré-visualizações de mídia.
filter.adult.label: Escolha se você deseja exibir NSFW
local_and_federated: Local e federado
filter.fields.only_names: Apenas nomes
header_logo: Logotipo do cabeçalho
mercure_enabled: Mercure ativado
report_issue: Relatar problema
infinite_scroll_help: Carregar automaticamente mais conteúdo quando chegar ao
final da página.
filter.fields.names_and_descriptions: Nomes e descrições
kbin_bot: Agente Mbin
up_votes: Boosts
enter_your_post: Insira sua publicação
related_posts: Publicações relacionadas
random_posts: Publicações aleatórias
federated_magazine_info: Esta revista é de um servidor federado e pode estar
incompleta.
disconnected_magazine_info: Esta revista não está recebendo atualizações (última
atividade %days% dia(s) atrás).
always_disconnected_magazine_info: Esta revista não está recebendo atualizações.
select_magazine: Selecione uma revista
collapse: Mostrar menos
flash_magazine_edit_success: A revista foi editada com sucesso.
set_magazines_bar_empty_desc: se o campo estiver vazio, as revistas ativas serão
exibidas na barra.
edited_comment: Editou um comentário
added_new_comment: Adicionou um novo comentário
edited_post: Editou uma publicação
replied_to_your_comment: Respondeu ao seu comentário
mod_remove_your_post: Um moderador removeu a sua publicação
added_new_reply: Adicionou uma nova resposta
change_downvotes_mode: Alterar o modo de votos negativos
downvotes_mode: Modo de votos negativos
notify_on_new_post_comment_reply: Respostas aos meus comentários em qualquer
publicação
notify_on_new_posts: Novas publicações de qualquer revista a que estou inscrito
flash_unmark_as_adult_success: A publicação foi desmarcada como NSFW.
too_many_requests: Limite ultrapassado, tente novamente mais tarde.
set_magazines_bar: Barra de revistas
mod_deleted_your_comment: Um moderador excluiu seu comentário
added_new_post: Adicionou uma nova publicação
wrote_message: Escreveu uma mensagem
removed: Removido por mod
deleted: Excluído pelo autor
mentioned_you: Mencionou você
all_magazines: Todas as revistas
create_new_magazine: Criar nova revista
add_new_post: Adicionar nova publicação
up_vote: Impulsionar
badges: Selos
joined: Membro desde
moderated: Moderado
reputation_points: Pontos de reputação
go_to_search: Ir para a busca
table_view: Visualização da tabela
tree_view: Visualização em árvore
report: Denunciar
edit_comment: Salvar alterações
edit_post: Editar publicação
show_profile_subscriptions: Mostrar assinaturas de revistas
featured_magazines: Revistas em destaque
votes: Votos
boosts: Boosts
show_magazines_icons: Mostrar ícones das revistas
flash_magazine_new_success: A revista foi criada com sucesso. Você pode
adicionar novo conteúdo ou explorar o painel de administração da revista.
flash_mark_as_adult_success: A publicação foi marcada como NSFW.
post: Publicação
terms: Termos de Serviço
people_local: Local
page_width_fixed: Fixo
magazine_posting_restricted_to_mods_warning: Somente os moderadores podem criar
tópicos nesta revista
flash_posting_restricted_error: Criar fios é restrito aos moderadores desta
revista e você não é um deles
magazine_log_mod_removed: removeu um moderador
magazine_log_mod_added: adicionou um moderador
down_votes: Reduz
register: Criar conta
agree_terms: Concordo com os %terms_link_start%Termos e
Condições%terms_link_end% e a %policy_link_start%Política de
Privacidade%policy_link_end%
flash_thread_new_success: O tópico foi criado com sucesso e agora está visível
para outros usuários.
flash_thread_delete_success: O fio foi excluído com sucesso.
flash_thread_edit_success: O fio foi editado com sucesso.
flash_thread_pin_success: O fio foi fixado com sucesso.
flash_thread_unpin_success: O tópico foi desafixado com sucesso.
mod_log_alert: AVISO - O Modlog pode conter conteúdo desagradável ou perturbador
que foi removido pelos moderadores. Por favor, tenha cuidado.
banned: baniu você
show_top_bar: Mostrar barra superior
sticky_navbar: Barra de navegação fixa
federation: Federação
status: Status
sidebar_position: Posição da barra lateral
left: Esquerda
right: Direita
ban_hashtag_description: Ao banir uma hashtag, você impedirá a criação de
publicações com essa hashtag, além de ocultar as publicações existentes que a
utilizam.
unban_hashtag_btn: Desbanir Hashtag
unban_hashtag_description: Ao desbanir uma hashtag, você poderá criar novamente
publicações com essa hashtag. As publicações existentes com essa hashtag não
serão mais ocultadas.
filters: Filtros
add_moderator: Adicionar moderador
add_badge: Adicionar selo
change_magazine: Mudar a revista
change_language: Alterar idioma
mark_as_adult: Marcar como NSFW
users: Usuários
writing: Escrevendo
meta: Meta
contact_email: E-mail de contato
active_users: Pessoas ativas
related_magazines: Revistas relacionadas
random_magazines: Revistas aleatórias
kbin_intro_desc: é uma plataforma descentralizada para agregação de conteúdo e
microblogging que opera dentro da rede Fediverso.
kbin_promo_desc: '%link_start%Clone o repositório%link_end% e desenvolva o fediverso'
your_account_is_not_active: Sua conta não foi ativada. Verifique seu e-mail para
obter instruções de ativação da conta ousolicite um
novo e-mail de ativação da conta.
toolbar.code: Código
toolbar.bold: Negrito
toolbar.italic: Itálico
toolbar.header: Cabeçalho
toolbar.quote: Citação
toolbar.strikethrough: Tachado
federated_search_only_loggedin: Pesquisa federada limitada se você não estiver
logado
federation_page_allowed_description: Instâncias conhecidas com as quais
federamos
federation_page_disallowed_description: Instâncias com as quais não nos
federamos
errors.server500.description: Desculpe, algo deu errado do nosso lado. Se você
continuar a ver esse erro, tente entrar em contato com o proprietário da
instância. Se essa instância não estiver funcionando, verifique enquanto isso
%link_start%outras instâncias do Mbin%link_end%, até que o problema seja
resolvido.
errors.server500.title: 500 Erro interno do servidor
resend_account_activation_email_description: Digite o endereço de e-mail
associado à sua conta. Nós enviaremos outro e-mail de ativação para você.
custom_css: CSS personalizado
oauth.consent.to_allow_access: Para permitir esse acesso, clique no botão
“Permitir” abaixo
oauth.consent.allow: Permitir
oauth.consent.deny: Negar
oauth.consent.app_requesting_permissions: gostaria de realizar as seguintes
ações em seu nome
oauth.client_identifier.invalid: ID de cliente OAuth inválido!
block: Bloquear
oauth.consent.app_has_permissions: já pode executar as seguintes ações
oauth2.grant.admin.all: Execute ações administrativas na sua instância.
oauth2.grant.delete.general: Exclua qualquer um dos seus fios, publicações ou
comentários.
oauth2.grant.write.general: Você pode criar ou editar qualquer um dos seus fios,
publicações ou comentários.
oauth2.grant.read.general: Leia todo o conteúdo ao qual você tem acesso.
oauth2.grant.entry.create: Criar novos fios.
oauth2.grant.entry.edit: Edite os fios existentes.
oauth2.grant.entry.all: Crie, edite ou exclua seus fios e vote, impulsione ou
denuncie qualquer um deles.
oauth2.grant.post.report: Denuncie qualquer publicação.
oauth2.grant.moderate.entry.set_adult: Marcar os fios como NSFW nas suas
revistas moderadas.
oauth2.grant.moderate.entry.trash: Jogar no lixo ou restaurar fios nas suas
revistas moderadas.
tags: Tags
articles: Fios
notify_on_new_post_reply: Qualquer nível de resposta a publicações de minha
autoria
mod_remove_your_thread: Um moderador removeu o seu fio
purge: Purgar
instances: Instâncias
preview: Visualizar
random_entries: Fios aleatórios
federation_page_dead_title: Instâncias mortas
errors.server429.title: 429 Requisições em excesso
oauth2.grant.moderate.magazine.reports.all: Gerencie as denúncias de suas
revistas moderadas.
ignore_magazines_custom_css: Ignorar o CSS personalizado das revistas
oauth2.grant.moderate.magazine.reports.read: Leia as denúncias nas suas revistas
moderadas.
oauth2.grant.moderate.magazine_admin.moderators: Adicione ou remova moderadores
das suas revistas.
oauth2.grant.report.general: Denunciar fios, publicações ou comentários.
oauth2.grant.moderate.magazine_admin.edit_theme: Edite o CSS personalizado das
suas revistas.
oauth2.grant.vote.general: Você pode votar a favor, contra ou impulsionar fios,
publicações ou comentários.
oauth2.grant.post.all: Crie, edite ou exclua seus microblogs e vote, impulsione
ou denuncie qualquer microblog.
oauth2.grant.magazine.block: Bloqueie ou desbloqueie revistas e veja as revistas
que você bloqueou.
oauth2.grant.user.message.read: Leia suas mensagens.
oauth2.grant.moderate.post.all: Moderar as publicações nas suas revistas
moderadas.
approve: Aprovar
approved: Aprovado
rejected: Rejeitado
created: Criado
expires: Expira em
bans: Banimentos
months: Meses
content: Conteúdo
week: Semana
month: Mês
year: Ano
add_new_article: Adicionar novo fio
subscriptions: Assinaturas
notify_on_new_entry_comment_reply: Respostas aos meus comentários em qualquer
fio
removed_comment_by: removeu um comentário de
restored_comment_by: restaurou o comentário de
removed_post_by: removeu um post de
restored_post_by: restaurou uma publicação de
he_banned: banido
he_unbanned: desbanir
set_magazines_bar_desc: adicionar os nomes da revista após a vírgula
added_new_thread: Novo fio adicionado
comment: Comentário
send_message: Enviar mensagem direta
message: Mensagem
infinite_scroll: Rolagem infinita
from_url: Do URL
upload_file: Fazer upload do arquivo
magazine_panel: Painel de revista
reject: Rejeitar
ban: Banir
add_ban: Adicionar banimento
unban: Desbanir
ban_hashtag_btn: Banir Hashtag
perm: Permanente
expired_at: Expirou em
change: Alterar
trash: Lixeira
icon: Ícone
done: Feito
pin: Fixar
unpin: Desafixar
unmark_as_adult: Desmarcar como NSFW
pinned: Fixado
article: Fio
reputation: Reputação
note: Nota
weeks: Semanas
federated: Federado
local: Local
admin_panel: Painel de administração
dashboard: Painel de controle
instance: Instância
pages: Páginas
toolbar.mention: Menção
email.delete.description: O usuário a seguir solicitou a exclusão de sua conta
resend_account_activation_email: Reenviar email de ativação da conta
unblock: Desbloquear
oauth2.grant.moderate.magazine.trash.read: Veja o conteúdo descartado em suas
revistas moderadas.
oauth2.grant.moderate.magazine_admin.all: Crie, edite ou exclua as revistas que
você possui.
oauth2.grant.user.profile.edit: Edite o seu perfil.
oauth2.grant.user.profile.read: Leia o seu perfil.
oauth2.grant.moderate.post.trash: Eliminar ou restaurar publicações nas suas
revistas moderadas.
oauth2.grant.moderate.post_comment.set_adult: Marcar comentários em publicações
como NSFW nas suas revistas moderadas.
continue_with: Continue com
account_deletion_button: Excluir conta
account_deletion_immediate: Excluir imediatamente
more_from_domain: Mais do domínio
federation_page_enabled: Página da federação ativada
errors.server404.title: 404 Não encontrado
toolbar.spoiler: Spoiler
oauth2.grant.moderate.magazine.ban.delete: Desbanir usuários de suas revistas
moderadas.
oauth2.grant.moderate.magazine_admin.delete: Exclua as revistas que você possui.
oauth2.grant.moderate.magazine_admin.update: Edite as regras, a descrição, o
status NSFW ou o ícone de qualquer uma das revistas que você possui.
oauth2.grant.admin.entry.purge: Exclua completamente qualquer fio de sua
instância.
oauth2.grant.block.general: Bloqueie ou desbloqueie qualquer revista, domínio ou
usuário e veja quais foram bloqueados.
oauth2.grant.domain.all: Assine ou bloqueie domínios e visualize os domínios que
você assinou ou bloqueou.
oauth2.grant.domain.block: Bloqueie ou desbloqueie domínios e visualize os
domínios que você bloqueou.
oauth2.grant.entry.delete: Exclua os fios existentes.
oauth2.grant.entry.vote: Vote a favor, contra ou dê um impulso em qualquer fio.
oauth2.grant.entry.report: Denunciar qualquer fio.
oauth2.grant.entry_comment.create: Criar novos comentários nos fios.
oauth2.grant.entry_comment.edit: Edite seus comentários existentes nos fios.
oauth2.grant.entry_comment.delete: Exclua seus comentários existentes nos fios.
oauth2.grant.entry_comment.report: Denunciar comentário em um fio.
oauth2.grant.magazine.all: Assine ou bloqueie revistas e veja as revistas
assinadas ou bloqueadas.
oauth2.grant.post.create: Criar novas publicações.
oauth2.grant.post.edit: Edite suas publicações existentes.
oauth2.grant.post.delete: Exclua suas publicações existentes.
oauth2.grant.post.vote: Dê um voto positivo, negativo ou um impulso em uma
publicação.
oauth2.grant.post_comment.all: Crie, edite ou exclua seus comentários nas
publicações e vote, impulsione ou denuncie qualquer comentário em uma
publicação.
oauth2.grant.post_comment.create: Crie novos comentários nas publicações.
oauth2.grant.post_comment.edit: Edite seus comentários existentes em
publicações.
oauth2.grant.post_comment.delete: Exclua seus comentários existentes em
publicações.
oauth2.grant.user.message.all: Leia suas mensagens e envie mensagens para outros
usuários.
oauth2.grant.user.message.create: Envie mensagens para outros usuários.
oauth2.grant.user.notification.all: Leia e limpe suas notificações.
oauth2.grant.user.notification.read: Leia suas notificações, inclusive as de
mensagens.
oauth2.grant.user.notification.delete: Limpe suas notificações.
oauth2.grant.user.follow: Siga ou deixe de seguir usuários e veja uma lista dos
usuários que você segue.
oauth2.grant.user.block: Bloquear ou desbloquear usuários e ver a lista de
usuários bloqueados.
oauth2.grant.moderate.all: Execute ações de moderação para as quais você tem
permissão em suas revistas moderadas.
oauth2.grant.moderate.entry.pin: Fixar os tópicos na parte superior das revistas
moderadas.
oauth2.grant.moderate.entry_comment.all: Moderar comentários em fios nas suas
revistas moderadas.
oauth2.grant.moderate.entry_comment.change_language: Altere o idioma dos
comentários nos fios das suas revistas moderadas.
oauth2.grant.moderate.post_comment.all: Moderar comentários nas publicações das
suas revistas moderadas.
show_related_posts: Mostrar publicações aleatórias
someone: Alguém
report_accepted: Uma denúncia foi aceita
related_entry: Relacionado
ban_expired: Banimento expirado
on: Em
off: Desligado
subject_reported: O conteúdo foi denunciado.
browsing_one_thread: Você está navegando apenas em um fio da discussão! Todos os
comentários estão disponíveis na página da publicação.
tag: Tag
eng: ENG
notify_on_new_entry: Novos fios (links ou artigos) em qualquer revista da qual
eu seja assinante
notify_on_new_entry_reply: Qualquer nível de comentários nos fios de minha
autoria
removed_thread_by: removeu um fio de
restored_thread_by: restaurou um fio de
toolbar.ordered_list: Lista ordenada
errors.server403.title: 403 Proibido
resend_account_activation_email_question: Conta inativa?
resend_account_activation_email_error: Houve um problema ao enviar esta
solicitação. Talvez você não tenha uma conta associada a esse e-mail ou talvez
ele já esteja ativado.
resend_account_activation_email_success: Se você tiver uma conta associada a
esse e-mail, enviaremos um novo e-mail de ativação.
oauth2.grant.domain.subscribe: Assine ou cancele a assinatura de domínios e
visualize os domínios que você assinou.
related_tags: Tags relacionadas
oc: OC
oauth.consent.title: Formulário de Consentimento OAuth2
private_instance: Forçar os usuários a fazer login antes de poderem acessar
qualquer conteúdo
oauth2.grant.moderate.magazine.reports.action: Aceite ou rejeite denúncias em
suas revistas moderadas.
oauth2.grant.moderate.magazine_admin.create: Criar novas revistas.
oauth2.grant.moderate.magazine_admin.badges: Crie ou remova selos das revistas
que você possui.
oauth2.grant.moderate.magazine_admin.stats: Veja o conteúdo, vote e veja as
estatísticas das revistas que você possui.
oauth2.grant.moderate.magazine_admin.tags: Crie ou remova tags das revistas que
você possui.
oauth2.grant.subscribe.general: Assine ou siga qualquer revista, domínio ou
usuário e veja em quais você se inscreveu.
oauth2.grant.entry_comment.all: Crie, edite ou exclua seus comentários nos fios
e vote, impulsione ou denuncie quaisquer comentários em um fio.
oauth2.grant.entry_comment.vote: Dê um voto positivo, negativo ou impulsione
qualquer comentário em um fio.
oauth2.grant.magazine.subscribe: Assine ou cancele a assinatura de revistas e
veja as revistas que você assinou.
oauth2.grant.post_comment.vote: Dê um voto positivo, negativo ou impulsione um
comentário em uma publicação.
oauth2.grant.user.oauth_clients.all: Leia e edite as permissões que você
concedeu a outros aplicativos OAuth2.
oauth2.grant.user.oauth_clients.read: Leia as permissões que você concedeu a
outros aplicativos OAuth2.
oauth2.grant.user.oauth_clients.edit: Edite as permissões que você concedeu a
outros aplicativos OAuth2.
oauth2.grant.moderate.entry.all: Moderar fios em suas revistas moderadas.
oauth2.grant.moderate.entry.change_language: Altere o idioma dos fios em suas
revistas moderadas.
page_width: Largura da página
page_width_max: Max
page_width_auto: Automático
oauth2.grant.moderate.entry_comment.trash: Eliminar ou restaurar comentários em
fios das suas revistas moderadas.
oauth2.grant.moderate.post.change_language: Altere o idioma das publicações das
suas revistas moderadas.
oauth2.grant.moderate.post.set_adult: Marcar as publicações como NSFW nas suas
revistas moderadas.
show_active_users: Mostrar usuários ativos
cake_day: Dia do bolo
oauth2.grant.moderate.post_comment.change_language: Alterar o idioma dos
comentários das publicações nas suas revistas moderadas.
own_content_reported_accepted: Uma denúncia do seu conteúdo foi aceita.
restrict_magazine_creation: Restringir a criação de revistas locais a
administradores e moderadores globais
report_subject: Assunto
own_report_accepted: Sua denúncia foi aceita
own_report_rejected: Uma denúncia foi rejeitada
direct_message: Mensagem direta
last_updated: Última atualização
magazine_log_entry_unpinned: removida a entrada fixada
email_confirm_button_text: Confirme seu pedido de alteração de senha
email_confirm_link_help: Como alternativa, você pode copiar e colar o que se
segue em seu navegador
email.delete.title: Pedido de exclusão da conta do usuário
oauth.consent.grant_permissions: Conceder Permissões
oauth.client_not_granted_message_read_permission: Este aplicativo não recebeu
permissão para ler suas mensagens.
restrict_oauth_clients: Restringir a criação de clientes OAuth2 a
administradores
oauth2.grant.moderate.entry_comment.set_adult: Marcar comentários em fios como
NSFW nas suas revistas moderadas.
notification_title_new_report: Uma nova denúncia foi criada
server_software: Software do servidor
edit_entry: Editar fio
edited_thread: Editou um fio
and: e
show_related_magazines: Mostrar revistas aleatórias
related_entries: Fios relacionados
magazine_panel_tags_info: Preencha apenas se você quiser que o conteúdo do
fediverso seja incluído nesta revista com base em tags
add_mentions_entries: Adicionar tags de menção nos fios
add_mentions_posts: Adicionar tags de menção em publicações
your_account_has_been_banned: Sua conta foi banida
toolbar.link: Link
toolbar.image: Imagem
toolbar.unordered_list: Lista desordenada
boost: Dar Boost
tokyo_night: Noite em Tóquio
preferred_languages: Filtrar idiomas dos fios e publicações
filter.fields.label: Escolha os campos que você deseja pesquisar
bot_body_content: "Bem-vindo ao Agente Mbin! Esse Agente desempenha um papel crucial
na implementação do ActivityPub na Mbin. Ele garante que a Mbin possa se comunicar
e se federar com outras instâncias no fediverso.\n\nO ActivityPub é um protocolo
de padrão aberto que permite que plataformas descentralizadas de redes sociais se
comuniquem e interajam entre si. Ele permite que usuários em diferentes instâncias
(servidores) sigam, interajam e compartilhem conteúdo na rede social federada conhecida
como fediverso. Ele fornece uma maneira padronizada para que os usuários publiquem
conteúdo, sigam outros usuários e participem de interações sociais, como curtir,
compartilhar e comentar em fios ou publicações."
oauth2.grant.moderate.magazine.list: Leia uma lista de suas revistas moderadas.
account_deletion_title: Exclusão da conta
account_deletion_description: Sua conta será excluída em 30 dias, a menos que
você opte por excluir a conta imediatamente. Para restaurar sua conta dentro
de 30 dias, faça login com as mesmas credenciais de usuário ou entre em
contato com um administrador.
federation_page_dead_description: Instâncias em que não conseguimos entregar
pelo menos 10 atividades seguidas e em que a última entrega bem-sucedida foi
há mais de uma semana
oauth2.grant.post_comment.report: Denuncie qualquer comentário em uma
publicação.
oauth2.grant.user.all: Leia e edite seu perfil, mensagens ou notificações; Leia
e edite as permissões que você concedeu a outros aplicativos; siga ou bloqueie
outros usuários; visualize listas de usuários que você segue ou bloqueia.
oauth2.grant.user.profile.all: Leia e edite seu perfil.
unregister_push_notifications_button: Remover registro de push
register_push_notifications_button: Cadastre-se para notificações push
manually_approves_followers: Aprovar manualmente os seguidores
reported_user: Usuário denunciado
show_related_entries: Mostrar fios aleatórios
notification_title_edited_post: Uma publicação foi editada
notification_title_removed_post: Uma publicação foi removida
notification_title_new_post: Nova publicação
notification_title_message: Nova mensagem direta
notification_title_ban: Você foi banido
notification_title_edited_thread: Um fio foi editado
notification_title_removed_thread: Um fio foi removido
notification_title_new_thread: Novo fio
notification_title_new_reply: Nova resposta
notification_title_mention: Você foi mencionado
notification_title_edited_comment: Um comentário foi editado
notification_title_removed_comment: Um comentário foi removido
notification_title_new_comment: Novo comentário
test_push_message: Olá, mundo!
test_push_notifications_button: Teste as notificações por push
flash_magazine_theme_changed_success: Atualizou com sucesso a aparência da
revista.
open_url_to_fediverse: Abrir URL original
change_my_avatar: Modificar meu avatar
filter_labels: Filtrar Etiquetas
unsuspend_account: Cancelar a suspensão da conta
account_suspended: A conta foi suspensa.
account_unsuspended: A suspensão da conta foi cancelada.
deletion: Exclusão
cancel_request: Cancelar pedido
abandoned: Abandonado
ownership_requests: Solicitações de propriedade
sensitive_warning: Conteúdo sensível
sensitive_toggle: Alternar a visibilidade de conteúdo sensível
last_successful_deliver: Última entrega bem-sucedida
version: Versão
last_successful_receive: Última recepção bem-sucedida
last_failed_contact: Último contato que não deu certo
admin_users_inactive: Inativos
admin_users_active: Ativos
user_verify: Ativar conta
oauth2.grant.moderate.magazine.ban.create: Banir usuários nas suas revistas
moderadas.
oauth2.grant.admin.entry_comment.purge: Exclua completamente um comentário em
fios da sua instância.
oauth2.grant.admin.post.purge: Excluir completamente qualquer publicação de sua
instância.
oauth2.grant.moderate.magazine.ban.read: Visualizar usuários banidos em suas
revistas moderadas.
oauth2.grant.admin.user.ban: Banir ou desbanir usuários da sua instância.
oauth2.grant.admin.magazine.move_entry: Mover fios entre revistas da sua
instância.
oauth2.grant.admin.magazine.purge: Excluir completamente revistas da sua
instância.
oauth2.grant.admin.user.all: Banir, verificar ou excluir completamente os
usuários da sua instância.
oauth2.grant.admin.user.verify: Verificar usuários da sua instância.
oauth2.grant.admin.user.delete: Excluir usuários da sua instância.
comment_reply_position: Posição de resposta ao comentário
magazine_theme_appearance_custom_css: CSS personalizado que será aplicado quando
você visualizar o conteúdo da sua revista.
update_comment: Atualizar comentário
show_avatars_on_comments_help: Exibir/ocultar avatares de usuários ao visualizar
comentários em um único fio ou publicação.
moderation.report.ban_user_title: Banir Usuário
moderation.report.reject_report_confirmation: Você tem certeza de que deseja
rejeitar essa denúncia?
schedule_delete_account: Programar exclusão
schedule_delete_account_desc: Programar a exclusão dessa conta em 30 dias. Isso
ocultará o usuário e seu conteúdo, além de impedir que ele faça login.
2fa.code_invalid: O código de autenticação não é válido
cancel: Cancelar
2fa.enable: Configurar a autenticação de dois fatores
2fa.setup_error: Erro ao ativar a 2FA para a conta
magazine_is_deleted: A revista foi excluída. Você pode restaurá-la dentro de 30 dias.
user_suspend_desc: Suspender sua conta oculta seu conteúdo na instância, mas não
o remove permanentemente, e você pode restaurá-la a qualquer momento.
remove_subscriptions: Remover assinaturas
remove_following: Remover seguidor
apply_for_moderator: Candidatar-se a moderador
deleted_by_moderator: O fio, a publicação ou o comentário foi excluído pelo
moderador
announcement: Anúncio
keywords: Palavras-chave
delete_content: Excluir conteúdo
remove_schedule_delete_account: Remover a exclusão programada
two_factor_backup: Códigos de backup da autenticação de dois fatores
cards_view: Visualização dos cartões
oauth2.grant.admin.user.purge: Excluir completamente usuários da sua instância.
oauth2.grant.moderate.magazine.ban.all: Gerenciar usuários banidos em suas
revistas moderadas.
2fa.verify: Verificar
flash_email_failed_to_sent: O e-mail não pode ser enviado.
flash_user_edit_password_error: Não foi possível alterar a senha.
edit_my_profile: Editar meu perfil
hide: Ocultar
all_time: Todo o período
spoiler: Spoiler
oauth2.grant.moderate.magazine.all: Gerenciar banimentos, denúncias e visualizar
itens descartados em suas revistas moderadas.
oauth2.grant.admin.post_comment.purge: Excluir completamente uma publicação da
sua instância.
oauth2.grant.admin.instance.all: Visualizar e atualizar as configurações ou
informações da instância.
oauth2.grant.admin.instance.settings.all: Exibir ou atualizar as configurações
da sua instância.
oauth2.grant.admin.federation.read: Exibir a lista das instâncias desfederadas.
oauth2.grant.admin.oauth_clients.all: Visualizar ou revogar clientes OAuth2 que
existem em sua instância.
flash_post_pin_success: A publicação foi fixada com sucesso.
show_avatars_on_comments: Mostrar avatares de comentários
single_settings: Único
moderation.report.approve_report_confirmation: Você tem certeza de que deseja
aprovar esta denúncia?
subject_reported_exists: Esse conteúdo já foi denunciado.
oauth2.grant.moderate.post.pin: Fixe as publicações na parte superior de suas
revistas moderadas.
purge_content: Purgar conteúdo
delete_content_desc: Excluir o conteúdo do usuário, deixando as respostas de
outros usuários nos fios, publicações e comentários criados.
remove_schedule_delete_account_desc: Remover a exclusão programada. Todo o
conteúdo estará disponível novamente e o usuário poderá fazer login.
2fa.authentication_code.label: Código de Autenticação
2fa.disable: Desativar a autenticação de dois fatores
2fa.backup: Seus códigos de backup de dois fatores
2fa.backup-create.help: Você pode criar novos códigos de autenticação de backup;
ao fazer isso, os códigos existentes serão invalidados.
2fa.backup-create.label: Criar novos códigos de autenticação de backup
2fa.remove: Remover 2FA
2fa.verify_authentication_code.label: Inserir um código de dois fatores para
verificar a configuração
2fa.qr_code_link.title: Ao acessar este link, você permite que sua plataforma
registre essa autenticação de dois fatores
2fa.backup_codes.recommendation: Recomenda-se que você mantenha uma cópia deles
em um local seguro.
password_and_2fa: Senha e 2FA
show_subscriptions: Mostrar assinaturas
alphabetically: Por ordem alfabética
subscriptions_in_own_sidebar: Em uma barra lateral separada
sidebars_same_side: Barras laterais no mesmo lado
subscription_sidebar_pop_out_right: Mover para a barra lateral separada à
direita
subscription_sidebar_pop_out_left: Mover para a barra lateral separada à
esquerda
subscription_panel_large: Painel grande
subscription_header: Revistas Assinadas
close: Fechar
position_bottom: Inferior
flash_image_download_too_large_error: Não foi possível criar a imagem, pois ela
é muito grande (tamanho máximo %bytes%)
flash_post_new_error: Não foi possível criar a publicação. Algo deu errado.
flash_magazine_theme_changed_error: Não foi possível atualizar a aparência da
revista.
flash_comment_new_success: O comentário foi criado com sucesso.
flash_comment_edit_success: O comentário foi atualizado com sucesso.
flash_comment_edit_error: Não foi possível editar o comentário. Algo deu errado.
flash_user_settings_general_error: Não foi possível salvar as configurações do
usuário.
flash_user_edit_profile_error: Não foi possível salvar as configurações de
perfil.
flash_user_edit_email_error: Não foi possível alterar o e-mail.
flash_thread_edit_error: Não foi possível editar o fio. Algo deu errado.
flash_post_edit_error: Não foi possível editar a publicação.
change_my_cover: Modificar minha capa
account_settings_changed: As configurações da sua conta foram alteradas com
sucesso. Você precisará fazer login novamente.
magazine_deletion: Exclusão da revista
restore_magazine: Restaurar revista
purge_magazine: Purgar revista
suspend_account: Suspender conta
account_banned: A conta foi banida.
account_unbanned: A conta foi desbanida.
account_is_suspended: A conta do usuário está suspensa.
request_magazine_ownership: Solicitar a propriedade da revista
action: Ação
user_badge_op: OP
user_badge_admin: Administrador
deleted_by_author: O fio, a publicação ou o comentário foi excluído pelo autor
sensitive_show: Clique para mostrar
sensitive_hide: Clique para ocultar
details: Detalhes
show: Exibir
edited: editado
sso_registrations_enabled.error: Novos registros de conta com gerenciadores de
identidade de terceiros estão desativados no momento.
sso_only_mode: Restringir o login e o registro apenas aos métodos de SSO
magazine_posting_restricted_to_mods: Restringir a criação de fios aos
moderadores
new_user_description: Este usuário é novo (ativo há menos de %days% dias)
admin_users_suspended: Suspensos
admin_users_banned: Banidos
max_image_size: Tamanho máximo do arquivo
cards: Cartões
oauth2.grant.moderate.post_comment.trash: Remover ou restaurar comentários de
publicações em suas revistas moderadas.
oauth2.grant.admin.magazine.all: Mover fios entre revistas ou excluí-las
completamente da sua instância.
oauth2.grant.admin.instance.stats: Veja as estatísticas da sua instância.
oauth2.grant.admin.federation.all: Exibir e atualizar instâncias atualmente
desfederadas.
oauth2.grant.admin.federation.update: Adicionar ou remover instâncias de ou para
a lista de instâncias desfederadas.
oauth2.grant.admin.oauth_clients.read: Veja os clientes OAuth2 que existem em
sua instância e suas estatísticas de uso.
last_active: Última atividade
magazine_theme_appearance_icon: Ícone personalizado para a revista. Se você não
selecionar nenhum, será usado o ícone padrão.
purge_content_desc: Purgar completamente o conteúdo do usuário, incluindo
excluir as respostas de outros usuários em fios, publicações e comentários
criados.
delete_account_desc: Excluir a conta, incluindo as respostas de outros usuários
em fios, publicações e comentários criados.
two_factor_authentication: Autenticação de dois fatores
2fa.add: Adicionar à minha conta
2fa.qr_code_img.alt: Um código QR que permite a configuração da autenticação de
dois fatores para sua conta
2fa.user_active_tfa.title: O usuário tem a 2FA ativada
2fa.available_apps: Use um aplicativo de autenticação de dois fatores, como
%google_authenticator%, %aegis% (Android) ou %raivo% (iOS) para fazer a
leitura do código QR.
2fa.backup_codes.help: Você pode usar esses códigos quando não tiver seu
dispositivo ou aplicativo de autenticação de dois fatores. Você não os
verá novamente e poderá usar cada um deles apenas uma
vez .
subscription_sort: Ordenar
flash_thread_tag_banned_error: Não foi possível criar o fio. O conteúdo não é
permitido.
flash_email_was_sent: O email foi enviado com sucesso.
flash_post_new_success: A publicação foi criada com sucesso.
flash_comment_new_error: Não foi possível criar o comentário. Algo deu errado.
flash_user_edit_profile_success: As configurações do perfil do usuário foram
salvas com sucesso.
flash_post_edit_success: A publicação foi editada com sucesso.
auto: Automático
delete_magazine: Excluir revista
sso_show_first: Mostrar o SSO primeiro nas páginas de login e registro
new_magazine_description: Esta revista é nova (ativa há menos de %days% dias)
comment_not_found: Comentário não encontrado
oauth2.grant.admin.oauth_clients.revoke: Revogar o acesso a clientes OAuth2 na
sua instância.
flash_post_unpin_success: A publicação foi desafixada com sucesso.
oauth2.grant.admin.instance.settings.read: Exibir as configurações da sua
instância.
oauth2.grant.admin.instance.settings.edit: Atualizar as configurações da sua
instância.
oauth2.grant.admin.instance.information.edit: Atualizar as páginas Sobre,
Perguntas frequentes, Contato, Termos de serviço e Política de privacidade da
sua instância.
magazine_theme_appearance_background_image: Imagem de fundo personalizada que
será aplicada quando você visualizar o conteúdo da sua revista.
moderation.report.approve_report_title: Aprovar a Denúncia
flash_user_settings_general_success: As configurações do usuário foram salvas
com sucesso.
accept: Aceitar
sso_registrations_enabled: Registros SSO ativados
back: Anterior
comment_reply_position_help: Exibir o formulário de resposta a comentários na
parte superior ou inferior da página. Quando a “rolagem infinita” estiver
ativada, a posição sempre aparecerá na parte superior.
moderation.report.reject_report_title: Rejeitar a Denúncia
moderation.report.ban_user_description: Você deseja banir o usuário (%username%)
que criou esse conteúdo desta revista?
subscription_sidebar_pop_in: Mover as assinaturas para o painel em linha
position_top: Topo
pending: Pendente
flash_account_settings_changed: As configurações da sua conta foram alteradas
com sucesso. Você precisará fazer login novamente.
flash_thread_new_error: Não foi possível criar o fio. Algo deu errado.
reported: denunciado
open_report: Abrir denúncia
remove_user_avatar: Remover avatar
remove_user_cover: Remover Capa
crosspost: Postagem cruzada
show_profile_followings: Mostrar usuários seguidos
notify_on_user_signup: Novas inscrições
ban_expires: Banimento expira
banner: Banner
type_search_term_url_handle: Digite termo de busca, URL ou identificador
viewing_one_signup_request: Você só está vendo um pedido de inscrição por
%username%
your_account_is_not_yet_approved: Sua conta ainda não foi aprovada. Lhe
enviaremos um e-mail assim que os administradores tiverem processado o seu
pedido de inscrição.
toolbar.emoji: Emoji
oauth2.grant.user.bookmark: Adicionar ou remover favorito
oauth2.grant.user.bookmark.add: Adicionar aos favoritos
oauth2.grant.user.bookmark.remove: Remover dos favoritos
oauth2.grant.user.bookmark_list: Leia, edite e exclua suas listas de favoritos
oauth2.grant.user.bookmark_list.read: Leia suas listas de favoritos
oauth2.grant.user.bookmark_list.edit: Edite suas listas de favoritos
oauth2.grant.user.bookmark_list.delete: Excluir suas listas de favoritos
oauth2.grant.moderate.entry.lock: Bloquear tópicos em suas revistas moderadas,
para que ninguém possa comentar sobre ele
oauth2.grant.moderate.post.lock: Bloquear microblogs em suas revistas moderadas,
para que ninguém possa comentar neles
bookmark_list_make_default: Tornar padrão
bookmark_list_create_placeholder: Digite o nome...
bookmarks_list_edit: Editar lista de favoritos
bookmark_list_create_label: Nome da lista
bookmark_list_edit: Editar
bookmark_list_selected_list: Lista selecionada
table_of_contents: Quadro de conteúdos
search_type_all: Tudo
search_type_entry: Tópicos
search_type_post: Microblogs
search_type_magazine: Revistas
search_type_user: Usuários
search_type_actors: Revistas + Usuários
search_type_content: Tópicos + Microblogs
select_user: Escolha um usuário
show_magazine_domains: Mostrar domínios de revistas
show_user_domains: Mostrar domínios de usuário
answered: respondidas
by: por
front_default_sort: Tipo padrão da página inicial
comment_default_sort: Tipo padrão de comentário
open_signup_request: Abrir pedidos de inscrição
image_lightbox_in_list: Miniaturas de tópicos abrem tela cheia
compact_view_help: Uma visão compacta com menos margens, onde a mídia é movida
para o lado direito.
show_users_avatars_help: Exibir a imagem de avatar do usuário.
show_magazines_icons_help: Exibir o ícone da revista.
show_thumbnails_help: Mostre as imagens da miniatura.
image_lightbox_in_list_help: Quando verificado, clicando na miniatura mostra uma
janela de caixa de imagem modal. Quando desmarcado, clicar na miniatura abrirá
o tópico.
show_new_icons: Mostrar novos ícones
show_new_icons_help: Mostrar ícone para nova revista / usuário (mais ou menos 30
dias)
magazine_instance_defederated_info: A instância desta revista não é federada. A
revista, portanto, não receberá atualizações.
user_instance_defederated_info: A instância deste usuário não é federada.
flash_thread_instance_banned: A instância desta revista está banida.
show_rich_mention: Menções populares
show_rich_mention_help: Renderize um componente de usuário quando um usuário for
mencionado. Isso incluirá o nome de exibição e a foto de perfil dele.
show_rich_mention_magazine: Menções populares de revistas
show_rich_mention_magazine_help: Renderize um componente de revista quando uma
revista for mencionada. Isso incluirá o nome de exibição e o ícone dela.
delete_magazine_icon: Excluir ícone da revista
flash_magazine_theme_icon_detached_success: Ícone da revista excluído com
sucesso
delete_magazine_banner: Excluir banner de revista
flash_magazine_theme_banner_detached_success: Banner de revista excluído com
sucesso
federation_uses_allowlist: Use a lista de permissões para federação
defederating_instance: Desfederar instância %i
their_user_follows: Quantidade de usuários de sua instância seguindo usuários em
nossa instância
our_user_follows: Quantidade de usuários de nossa instância seguindo usuários em
sua instância
their_magazine_subscriptions: Quantidade de usuários de sua instância inscritos
em revistas em nossa instância
our_magazine_subscriptions: Quantidade de usuários em nossa instância inscritos
em revistas de sua instância
confirm_defederation: Confirmar desfederação
flash_error_defederation_must_confirm: Você tem que confirmar a desfederação
allowed_instances: Instâncias permitidas
btn_deny: Recusar
btn_allow: Permitir
ban_instance: Banir instância
allow_instance: Permitir instância
federation_page_use_allowlist_help: Se uma lista de permissão for usada, essa
instância somente irá federar com as instâncias explicitamente permitidas.
Caso contrário, esta instância irá federar com cada instância, exceto aqueles
que são proibidos.
you_have_been_banned_from_magazine: Você foi banido da revista %m.
you_have_been_banned_from_magazine_permanently: Você foi permanentemente banido
da revista %m.
you_are_no_longer_banned_from_magazine: Você não está mais banido da revista %m.
front_default_content: Visão padrão da página inicial
default_content_default: Predefinição do servidor (Tópicos)
default_content_combined: Tópicos + Microblog
default_content_threads: Tópicos
default_content_microblog: Microblog
combined: Combinado
sidebar_sections_random_local_only: Restringir seções da barra lateral
"Tópicos/Postagens aleatórios" para apenas local
sidebar_sections_users_local_only: Restringir seção de barra lateral "pessoas
ativas" apenas local
random_local_only_performance_warning: Habilitar "Apenas local aleatoriamente"
pode causar impacto de desempenho SQL.
discoverable: Descobrível
user_discoverable_help: Se isso estiver ativado, seu perfil, tópicos, microblogs
e comentários podem ser encontrados através da pesquisa e os painéis
aleatórios. Seu perfil também pode aparecer no painel de usuário ativo e na
página de pessoas. Se isso for desativado, seus posts ainda serão visíveis
para outros usuários, mas eles não aparecerão no feed todo.
magazine_discoverable_help: Se isso estiver ativado, esta revista e tópicos,
microblogs e comentários desta revista podem ser encontrados através da
pesquisa e os painéis aleatórios. Se isso for desativado, a revista ainda
aparecerá na lista de revistas, mas os fios e microblogs não aparecerão em
todo o feed.
flash_thread_lock_success: Tópico bloqueado com sucesso
flash_thread_unlock_success: Tópico desbloqueado com sucesso
flash_post_lock_success: Microblog bloqueado com sucesso
flash_post_unlock_success: Microblog desbloqueado com sucesso
lock: Fechado
unlock: Reabrir
comments_locked: Os comentários estão fechado.
magazine_log_entry_locked: fechar os comentários de
magazine_log_entry_unlocked: reabrir os comentários de
modlog_type_entry_lock: Tópico fechado
modlog_type_entry_unlock: Tópico reaberto
modlog_type_post_lock: Microblog fechado
modlog_type_post_unlock: Microblog reaberto
contentnotification.muted: Mutar | não receber notificações
contentnotification.default: Padrão | obter notificações de acordo com suas
configurações padrão
contentnotification.loud: Geral | obter todas as notificações
indexable_by_search_engines: Indexável por motores de busca
user_indexable_by_search_engines_help: Se esta configuração é falsa, os motores
de busca são aconselhados a não indexar qualquer um dos seus tópicos e
microblogs, no entanto, seus comentários não são afetados por este e maus
atores podem ignorá-lo. Esta configuração também é federada para outros
servidores.
magazine_indexable_by_search_engines_help: Se esta configuração é falsa, os
motores de busca são aconselhados a não indexar nenhum dos tópicos e
microblogs nestas revistas. Isso inclui a landing page e todas as páginas de
comentários. Esta configuração também é federada para outros servidores.
magazine_name_as_tag: Use o nome da revista como uma tag
magazine_name_as_tag_help: As tags de uma revista são usadas para combinar
postagens microblog para esta revista. Por exemplo, se o nome é "fediverso" e
as tags da revista contêm "fediverso", então cada post microblog contendo
"#fediverso" será colocado nesta revista.
magazine_theme_appearance_banner: Banner personalizado para a revista. Ele é
exibido acima de todos os fios e deve estar em uma relação de aspecto amplo
(5:1, ou 1500px * 300px).
2fa.manual_code_hint: Se você não puder digitalizar o QR code, digite o segredo
manualmente
flash_thread_ref_image_not_found: A imagem referenciada por 'imageHash' não pôde
ser encontrada.
moderator_requests: Pedidos de Mod
user_badge_global_moderator: Mod Global
user_badge_moderator: Mod
user_badge_bot: Bot
reporting_user: Reportando o usuário
magazine_log_entry_pinned: Entrada fixada
notification_title_new_signup: Um novo usuário registrado
notification_body_new_signup: O usuário %u% registrado.
notification_body2_new_signup_approval: Você precisa aprovar o pedido antes que
eles possam fazer login
bookmark_add_to_list: Adicionar favorito a %list%
bookmark_remove_from_list: Remover favorito de %list%
bookmark_remove_all: Remover todos os favoritos
bookmark_add_to_default_list: Adicionar favorito à lista padrão
bookmark_lists: Listas de favoritos
bookmarks: Favoritos
bookmarks_list: Favoritos na %list%
count: Contagem
is_default: É o padrão
bookmark_list_is_default: É a lista padrão
bookmark_list_create: Criar
new_users_need_approval: Novos usuários têm que ser aprovados por um
administrador antes que eles possam fazer login.
signup_requests: Pedidos de inscrição
application_text: Explique por que você quer participar
signup_requests_header: Pedidos de inscrição
signup_requests_paragraph: Esses usuários gostariam de se juntar ao seu
servidor. Não podem entrar até aprovar o pedido de inscrição.
flash_application_info: Um administrador precisa aprovar sua conta antes de
poder fazer login. Você receberá um e-mail assim que o pedido de inscrição for
processado.
email_application_approved_title: Seu pedido de inscrição foi aprovado
email_application_approved_body: Seu pedido de inscrição foi aprovado pelo
administrador do servidor. Agora você pode fazer login no servidor em %siteName% .
email_application_rejected_title: Seu pedido de inscrição foi rejeitado
email_application_rejected_body: Obrigado pelo seu interesse, mas lamentamos
informar que o seu pedido de inscrição foi recusado.
email_application_pending: Sua conta requer aprovação do administrador antes de
poder fazer login.
email_verification_pending: Você tem que verificar seu endereço de e-mail antes
de fazer login.
show_rich_ap_link: Múltiplos PA links
show_rich_ap_link_help: Renderiza um componente embutido quando outro conteúdo
do ActivityPub estiver vinculado.
attitude: Atitude
type_search_magazine: Limitar a busca à revista...
type_search_user: Limitar a busca ao autor...
modlog_type_entry_deleted: Tópico apagado
modlog_type_entry_restored: Tópico restaurado
modlog_type_entry_comment_deleted: Comentário do Tópico excluído
modlog_type_entry_comment_restored: Comentário de Tópico restaurado
modlog_type_entry_pinned: Tópico fixado
modlog_type_entry_unpinned: Tópico desfixado
modlog_type_post_deleted: Microblog excluído
modlog_type_post_restored: Microblog restaurado
modlog_type_post_comment_deleted: Resposta do microblog apagada
modlog_type_post_comment_restored: Resposta do microblog restaurada
modlog_type_ban: Usuário baniddo da revista
modlog_type_moderator_add: Moderador de revista adicionado
modlog_type_moderator_remove: Moderador de revista removido
everyone: Todo mundo
nobody: Ninguém
followers_only: Apenas seguidores
direct_message_setting_label: Quem pode enviar uma mensagem direta
================================================
FILE: translations/messages.ru.yaml
================================================
type.link: Ссылка
type.article: Ветка
type.photo: Фото
type.video: Видео
type.smart_contract: Умный контракт
type.magazine: Журнал
thread: Ветка
threads: Ветки
microblog: Микроблог
people: Люди
events: События
magazine: Журнал
magazines: Журналы
search: Поиск
add: Добавить
select_channel: Выберите канал
login: Войти
top: Лучшее
hot: Горячее
active: Активное
newest: Свежее
oldest: Более старое
commented: Оставлен комментарий
change_view: Изменить вид
filter_by_time: Сортировка по времени
filter_by_type: Сортировка по типу
comments_count: '{0}Комментариев|{1}Комментарий|]1,Inf[ Комментариев'
favourites: Положительные оценки
favourite: Нравиться
more: Больше
avatar: Аватар
added: Добавлено
up_votes: Голос За
down_votes: Голос против
no_comments: Нет комментариев
created_at: Создано
owner: Владелец
subscribers: Подписки
online: Онлайн
comments: Комментарии
posts: Посты
replies: Ответы
moderators: Модераторы
mod_log: Лог модерации
add_comment: Добавить комментарий
add_post: Добавить пост
add_media: Добавить медиа
markdown_howto: Как работает наш редактор?
enter_your_comment: Введите комментарий
enter_your_post: Введите содержимое поста
activity: Активность
cover: Обложка
related_posts: Связанные посты
random_posts: Случайный пост
federated_magazine_info: Данный журнал из другого инстанса и информация может
быть неполной.
federated_user_info: Данный профиль из другого инстанса и информация может быть
неполной.
go_to_original_instance: Открыть на удалённом сервере
empty: Здесь ничего нет
subscribe: Подписка
unsubscribe: Отписки
follow: Подписаться
unfollow: Отписаться
reply: Ответить
login_or_email: Логин или почта
password: Пароль
remember_me: Запомнить меня
dont_have_account: Еще нет аккаунта?
you_cant_login: Не можете войти?
already_have_account: Уже есть аккаунт?
register: Регистрация
reset_password: Сбросить пароль
show_more: Показать больше
to: к
in: в
username: Имя пользователя
email: Почта
repeat_password: Повторить пароль
agree_terms: Принять %terms_link_start%Условия использования%terms_link_end% и
%policy_link_start%Политику конфиденциальности%policy_link_end%
terms: Условия использования
privacy_policy: Политика конфиденциальности
about_instance: Об инстансе
all_magazines: Все журналы
stats: Статистика
fediverse: Федерация
create_new_magazine: Создать новый журнал
add_new_article: Добавить новую ветку
add_new_link: Добавить новую ссылку
add_new_photo: Добавить новое фото
add_new_post: Добавить новый пост
add_new_video: Добавить новое видео
contact: Контакты
faq: ЧаВо
rss: RSS
change_theme: Изменить тему
useful: Полезное
help: Помощь
check_email: Проверьте свою почту
reset_check_email_desc: Если мы нашли аккаунт связанный с вашей почтой, То вы
должны получить ссылку для сброса пароля. Ссылка доступна до %expire%.
reset_check_email_desc2: Если вы не получили письмо проверьте папку со спамом
try_again: Попробовать еще раз
up_vote: Продвинуть
down_vote: Уменьшить
email_confirm_header: Привет! Подтвердите вашу почту.
email_confirm_content: 'Готов активировать аккаунт в Mbin? Переходи по ссылке ниже:'
email_verify: Проверить адрес почты
email_confirm_expire: Учтите что ссылка доступна лишь час.
email_confirm_title: Подтвердите название вашей почты.
select_magazine: Выберите журнал
add_new: Добавить новый
url: Ссылка
title: Заголовок
body: Тело
tags: Теги
badges: Бейджи
is_adult: 18+ / NSFW
eng: ENG
oc: OC
image: Изображение
image_alt: Другое изображение
name: Имя
description: Описание
rules: Правила
domain: Домен
followers: Подписчики
following: Подписываться
subscriptions: Подписки
overview: Обзор
cards: Карточки
columns: Колонки
user: Пользователь
joined: Присоединиться
moderated: Модерируется
people_local: Местные
people_federated: В федерации
reputation_points: Очки репутации
related_tags: Связанные теги
go_to_content: Перейти к контенту
go_to_filters: Перейти к фильтрам
go_to_search: Перейти к поиску
subscribed: Подписан
all: Все
logout: Выход
classic_view: Стандартное отображение
compact_view: Компактный вид
chat_view: Просмотр чата
tree_view: В виде дерева
table_view: Просмотр таблицы
cards_view: Просмотр карточек
3h: 3 часа
6h: 6 часов
12h: 12 часов
1d: 1 день
1w: 1 неделя
1m: 1 месяц
1y: 1 год
links: Ссылки
articles: Ветки
photos: Фоторграфии
videos: Видео
report: Отчёт
share: Поделиться
copy_url: Копировать Mbin URL
copy_url_to_fediverse: Копировать оригинальный URL
share_on_fediverse: Поделиться в Федерации
edit: Редактировать
are_you_sure: Вы уверены?
moderate: Модерация
reason: Причина
delete: Удалить
edit_post: Редактировать пост
edit_comment: Сохранить изменения
settings: Настройки
general: Основные
profile: Профиль
blocked: Заблокирован
reports: Отчёты
notifications: Уведомления
messages: Сообщения
appearance: Появление
homepage: Домашняя страница
hide_adult: Скрыть NSFW контент
featured_magazines: Избранные журналы
privacy: Приватное
show_profile_subscriptions: Показать журналы в подписках
show_profile_followings: Показать профили подписчиков
notify_on_new_entry_reply: Уведомление о новом ответе
notify_on_new_entry_comment_reply: Ответы на мои комментарии в любой ветке
notify_on_new_post_reply: Любой уровень ответа в моих постах
notify_on_new_post_comment_reply: Ответы на мои комментарии в любом посте
notify_on_new_entry: Новые ветки в любом журнале из моих подписок
notify_on_new_posts: Новые посты в любом журнале из моих подписок
save: Сохранить
about: О
old_email: Текущая электронная почта
new_email: Новая электронная почта
new_email_repeat: Подтвердить новую электронную почту
current_password: Текущий пароль
new_password: Новый пароль
new_password_repeat: Подтвердить новый пароль
change_email: Изменить электронную почту
change_password: Изменить пароль
expand: Развернуть
collapse: Свернуть
domains: Домены
error: Ошибка
votes: Голоса
theme: Тема
dark: Тёмный
light: Светлый
solarized_light: Солнечный светлый
solarized_dark: Солнечный тёмный
default_theme: Стандартная тема
default_theme_auto: Светлый/Тёмный (Определять автоматически)
solarized_auto: Солнечное (Определять автоматически)
font_size: Размер шрифта
size: Размер
boosts: Продвижение
show_users_avatars: Показать аватары пользователей
yes: Да
no: Нет
show_magazines_icons: Показать иконки журналов
show_thumbnails: Показать миниатюры
rounded_edges: Скруглить края
removed_thread_by: удалил ветвь, созданную
restored_thread_by: восстановил ветвь, созданную
removed_comment_by: Комментарий удалён
restored_comment_by: Комментарий восстановлен
removed_post_by: Пост удалён
restored_post_by: Пост восстановлен
he_banned: Бан
he_unbanned: Разбан
read_all: Прочитать всё
show_all: Показать всё
flash_register_success: Добро пожаловать! Ваша учетная запись зарегистрирована.
Последний шаг - Проверьте свою электронную почту и перейдите по ссылке для
активации Вашей учётной записи.
flash_thread_new_success: Ветка успешно создана и теперь видна другим
пользователям.
flash_thread_edit_success: Ветка успешно отредактирована.
flash_thread_delete_success: Ветка была успешно удалёна.
flash_thread_pin_success: Ветка успешно закреплена.
flash_thread_unpin_success: Ветка успешно откреплёна.
flash_magazine_new_success: Журнал успешно создан. Сейчас можно изучить панель
администратора и добавить новый контент.
flash_magazine_edit_success: Журнал успешно изменён.
flash_mark_as_adult_success: Пост отмечен как NSFW.
flash_unmark_as_adult_success: Пост больше не имеет отметки NSFW.
too_many_requests: Лимит запросов превышен, попробуйте ещё раз позднее.
set_magazines_bar: Активная панель журнала
set_magazines_bar_desc: Добавить названия журналов после запятой
set_magazines_bar_empty_desc: Если поле пустое, активные журналы отображаются на
активной панели.
mod_log_alert: ВНИМАНИЕ! - Данный материал может содержать неприятный или
опасный контент! Это было удалено модераторами. Пожалуйста, будьте
внимательны.
added_new_thread: Добавлен в новую ветку
edited_thread: Отредактированная ветка
mod_remove_your_thread: Модератор удалил вашу ветку
added_new_comment: Добавлен новый комментарий
edited_comment: Отредактированный комментарий
replied_to_your_comment: Ответил на ваш комментарий
mod_deleted_your_comment: Модератор удалил ваш комментарий
added_new_post: Добавлен новый пост
edited_post: Отредактированный пост
mod_remove_your_post: Модератор удалил ваш пост
added_new_reply: Добавлен новый ответ
wrote_message: Написал сообщение
banned: Забанил вас
removed: Перемещён модератором
deleted: Удалён автором
mentioned_you: Упомянул тебя
comment: Комментарий
post: Пост
ban_expired: Бан окончен
purge: Очистить
send_message: Написать личное сообщение
message: Сообщение
infinite_scroll: Бесконечная прокрутка
show_top_bar: Показать активную панель
sticky_navbar: Липкая панель навигации
subject_reported: Содержимое было зарегистрировано.
sidebar_position: Положение боковой панели
left: Слева
right: Справа
federation: Федерация
status: Статус
on: Включен
off: выключен
instances: Инстансы
upload_file: Загрузить файл
from_url: От отправителя
magazine_panel: Панель журнала
reject: Отклонить
approve: Согласиться
ban: Бан
filters: Фильтры
approved: Согласовано
rejected: Отклонённые
add_moderator: Добавить модератора
add_badge: Добавить обозначение
bans: Баны
created: Созданные
expires: Истекает
perm: Постоянный
expired_at: Истёк в
add_ban: Добавить в бан
trash: Мусор
icon: Иконка
done: Сделано
pin: Закрепление
unpin: Открепление
change_magazine: Выбрать журнал
change_language: Выбрать язык
mark_as_adult: Отметить NSFW
unmark_as_adult: Снять отметку NSFW
change: Выбрать
pinned: Закрепить
preview: Предпросмотр
article: Ветка
reputation: Репутация
note: Заметка
writing: Письмо
users: Пользователи
content: Контент
week: Неделя
weeks: Недели
month: Месяц
months: Месяцы
year: Годы
federated: Федеративный
local: Локальный
admin_panel: Панель администратора
dashboard: Панель инструментов
contact_email: Контактная электронная почта
meta: Meta
instance: Инстанс
pages: Страницы
FAQ: Часто задаваемые вопросы
type_search_term: Напишите вопрос
federation_enabled: Федерация включена
registrations_enabled: Регистрация включена
registration_disabled: Регистрация отключена
restore: Восстановление
add_mentions_entries: Добавить тэги в ветки
add_mentions_posts: Добавить тэги в посты
Password is invalid: Неправильный пароль.
Your account is not active: Ваш аккаунт не был активирован.
Your account has been banned: Ваш аккаунт был заблокирован.
firstname: Имя
send: Отправить
active_users: Активные пользователи
random_entries: Случайные ветки
related_entries: Связанные ветки
delete_account: Удалить учётную запись
purge_account: Очистить аккаунт
ban_account: Забанить аккаунт
unban_account: Разбанить аккаунт
related_magazines: Связанные журналы
random_magazines: Случайные журналы
magazine_panel_tags_info: Предоставить, только если вы хотите, чтобы контент из
Федиверса был включен в этот журнал на основе тегов
sidebar: Боковая панель
auto_preview: Авто предпросмотр
dynamic_lists: Динамические списки
banned_instances: Забаненные инстансы
kbin_intro_title: Посмотреть Федиверс
kbin_intro_desc: является децентрализованной платформой для агрегирования
контента и микроблогинга, которая работает в сети Fediverse.
kbin_promo_title: Создайте ваш инстанс
kbin_promo_desc: '%link_start%перейдите по ссылке%link_end% и начните свой проект'
captcha_enabled: Капча включена
header_logo: Логотип заголовка
browsing_one_thread: Вы просматриваете только одну ветку обсуждения! Все
комментарии доступны на странице поста.
return: Вернуться
boost: Продвигать
mercure_enabled: Включено
report_issue: Описание релиза
tokyo_night: Ночной Токио
preferred_languages: Языковая фильтрация в ветках и постах
infinite_scroll_help: Автоматически загружать контент, когда вы дочитаете
страницу до конца.
sticky_navbar_help: Навигационная панель будет находиться вверху, пока вы
прокручиваете вниз.
auto_preview_help: Автоматически увеличить поле просмотра медиа.
reload_to_apply: Перезагрузить страницу для принятия изменений
filter.origin.label: Выбрать оригинал
filter.fields.label: Выбрать поля для поиска
filter.adult.label: Выберите отображать или нет NSFW
filter.adult.hide: Скрыть NSFW
filter.adult.show: Показать NSFW
filter.adult.only: Только NSFW
local_and_federated: Локальный и федеративный
filter.fields.only_names: Только имена
filter.fields.names_and_descriptions: Имена и описания
kbin_bot: Mbin бот
bot_body_content: "Добро поажловать в Mbin Бот! Этот бот важный элемент при использовании
функционала ActivityPub в Mbin. Это гарантирует, что MBIN может общаться и быть
в Федерации с другими сервисами в Fediverse. \n\nActivityPub является открытым стандартом,
протокол, который позволяет децентрализованным социальным сетям платформ общаться
и взаимодействовать друг с другом. Это позволяет пользователям в разных инстансах
(серверы) взаимодействовать и делиться контентом в федеративной социальной сети,
известной как Fediverse."
password_confirm_header: Подтвердите запрос на изменение пароля.
your_account_is_not_active: Ваш аккаунт не может быть активирован. Пожалуйста,
проверьте инструкцию для активации в вашей электронной почте Сделать новый запрос для активации аккаунта.
your_account_has_been_banned: Баш аккаунт был забанен
toolbar.bold: Жирный шрифт
toolbar.italic: Италик
toolbar.strikethrough: Выделенный
toolbar.header: Заглавие
toolbar.quote: Цитировать
toolbar.code: Код
toolbar.link: Ссылка на панель инструментов
toolbar.image: Изображение
toolbar.unordered_list: Неуопрядоченный список
toolbar.ordered_list: Упорядоченный список
toolbar.mention: Упомянуть
federation_page_enabled: Страница федерации включена
federation_page_allowed_description: Известные инстансы Федерации
federation_page_disallowed_description: Инстансы не подключенные к Федерации
federated_search_only_loggedin: Ограниченный поиск в Федерации, если нет
регистрации
more_from_domain: Больше о домене
errors.server500.title: 500 - внутренняя ошибка сервера
errors.server500.description: Что-то пошло не так с нашей стороны. Если вы
продолжите видеть эту ошибку, попробуйте связаться с владельцем инстанса.
errors.server429.title: 429 - много запросов
errors.server404.title: 404 - страница не найдена
errors.server403.title: 403 - недоступно
email_confirm_button_text: Подтвердите ваш запрос на изменение пароля
email_confirm_link_help: Если не смогли перейти автоматически, скопируйте ссылку
и откройте в вашем барузере
email.delete.title: Запрос на удаление учётной записи
email.delete.description: Запрос на удаление учётной записи подписчика
resend_account_activation_email_question: Неактивный аккаунт?
resend_account_activation_email: Ещё раз отправить запрос на активацию аккаунта
resend_account_activation_email_error: Ошибка при отправке запроса. Запрос
отправлен ошибочно или аккаунт уже активирован.
resend_account_activation_email_success: Если учетная запись, связанная с этим
электронным письмом существует, мы отправим новое электронное письмо с
активацией.
resend_account_activation_email_description: Введите адрес электронной почты,
связанный с вашим аккаунтом. Мы отправим вам еще одно электронное письмо с
активацией.
custom_css: Пользовательский CSS
ignore_magazines_custom_css: Игнориовать журналы с пользовательстким CSS
oauth.consent.title: OAuth2 форма согласия
oauth.consent.grant_permissions: Предоставление разрешеий
oauth.consent.app_requesting_permissions: Разрешение выполнить дейтсвия от
вашего имени
oauth.consent.app_has_permissions: Можете выполнить следующие действия
oauth.consent.to_allow_access: Чтобы разрешить этот доступ, нажмите РАЗРЕШИТЬ
ниже
oauth.consent.allow: Разрешить
oauth.consent.deny: Запретить
oauth.client_identifier.invalid: Не верный ID OAuth!
oauth.client_not_granted_message_read_permission: У этого приложения нет
разрешения читать ваши сообщения.
restrict_oauth_clients: Ограничить создание OAuth2 Администарторами
block: Заблокировано
unblock: Разблокировано
oauth2.grant.moderate.magazine.ban.delete: Разбанить пользователей в
модерируемых журналах.
oauth2.grant.moderate.magazine.list: Прочитать список ваших модерируемых
журналов.
oauth2.grant.moderate.magazine.reports.all: Управлять отчётом в модерируемых
журналах.
oauth2.grant.moderate.magazine.reports.read: Читать отчёты в модерируемых
журналах.
oauth2.grant.moderate.magazine.reports.action: Принять или отклонить отчёты в
модерируемых журналах.
oauth2.grant.moderate.magazine.trash.read: Просмотреть удалённый контент в
модерируемых журналах.
oauth2.grant.moderate.magazine_admin.all: Создать, изменить или удалить
собственный журнал.
oauth2.grant.moderate.magazine_admin.create: Создать новый журнал.
oauth2.grant.moderate.magazine_admin.delete: удалить любой ваш журнал.
oauth2.grant.moderate.magazine_admin.update: Редактировать описание правил
любого из ваших журналов, статус или знак NSFW.
oauth2.grant.moderate.magazine_admin.edit_theme: Изменить пользовательский CSS в
вашем журнале.
oauth2.grant.moderate.magazine_admin.moderators: Добавить или удалить
модераторов в вашем журнале.
oauth2.grant.moderate.magazine_admin.badges: Создать или удалить значки в своём
журнале.
oauth2.grant.moderate.magazine_admin.tags: Создать или удалить тэги в своём
журнале.
oauth2.grant.moderate.magazine_admin.stats: Просмотреть контент, голоса и
статусы в своём журнале.
oauth2.grant.admin.all: Выполнять любые административные действия в отношени
вашего журнала.
oauth2.grant.admin.entry.purge: Полностью удалите любую ветку из вашего
инстанса.
oauth2.grant.read.general: Читать весь контент, к которому у вас есть доступ.
oauth2.grant.write.general: Создать или изменить любую вашу ветку, пост или
комментарий.
oauth2.grant.delete.general: Удалить любую вашу ветку, пост или комментарий.
oauth2.grant.report.general: Сообщить о ветках, постах или комментариях.
oauth2.grant.vote.general: Оценивать положительно, отрицательно или продвигать
ветки, записи и комментарии.
oauth2.grant.subscribe.general: Подпишитесь или подписывайтесь на любой журнал.
домен или пользователей и просматривайте журналы, домены и пользователей на
которых вы подписаны.
oauth2.grant.block.general: Блокируйте или разброкируйте любой журнал, домен или
пользователя и просматривайте журналы, домены и пользователей, которых вы
заблокировали.
oauth2.grant.domain.all: Подписывайтесь на домены или заблокируйте их, а также
просматривайте домены на которые вы подписаны или заблокровали.
oauth2.grant.domain.subscribe: Подписывайтесь или отписывайтель на домены, и
просматривайте домены на которые вы подписаны.
oauth2.grant.domain.block: Блокируйте или разблокируйте домены, и просмтривате
домены которые вы заблокировали.
oauth2.grant.entry.all: Создайте, редактируйте или удалте ваши ветки, и голоса,
продвижения, или сообщите о любых ветках.
oauth2.grant.entry.create: Создайте новую ветку.
oauth2.grant.entry.edit: Отредактируйте существующие ветки.
oauth2.grant.entry.delete: Удалите существующие ветки.
oauth2.grant.entry.vote: Оценивать положительно, отрицательно и продвигать
ветки.
oauth2.grant.entry.report: Сообщить о любых темах.
oauth2.grant.entry_comment.all: Создать, отредактировать или удалить любой
комментраий в ветке, проголосовавть, продвинуть или сообщить о любом
комментариив ветке.
oauth2.grant.entry_comment.create: Создать новый комментарий в ветке.
oauth2.grant.entry_comment.edit: Редактировать существующий комментарий в ветке.
oauth2.grant.entry_comment.delete: Удалить существующие комментарии в ветке.
oauth2.grant.entry_comment.vote: Оценивать положительно, отрицательно и
продвигать любые комментарии в ветках.
oauth2.grant.entry_comment.report: Сообщить о комментарии в ветке.
oauth2.grant.magazine.all: Подписаться на или заблокировать журнал, и
просматривать журналы на которые вы подписаны или заблокировали.
oauth2.grant.magazine.subscribe: Подписаться или отписаться от журнала, и
посмотреть журнал на который вы подписаны.
oauth2.grant.magazine.block: Блокировать или разблокировать журнал и посмотреть
жарнал, который вы блокируете.
oauth2.grant.post.all: Создать, редактировать или удалить ваш микроблог,
головать, продвинуть или сообщить о микроблоге.
oauth2.grant.post.create: Создать новый пост.
oauth2.grant.post.edit: Редактировать существующий пост.
oauth2.grant.post.delete: Удалить ваши существующие посты.
oauth2.grant.post.vote: Оценивать положительно, отрицательно и продвигать
записи.
oauth2.grant.post.report: Сщщбщить о любом посте.
oauth2.grant.post_comment.all: Сздать, редактировать или удалить ваши
комментарии к посту, и голосовать, продвигать или сообщить о комментраии к
посту.
oauth2.grant.post_comment.create: Создать новые комментарии к посту.
oauth2.grant.post_comment.edit: Редактировать ваши существующие комментарии к
посту.
oauth2.grant.post_comment.delete: Удалить ваши существующие комментарии к посту.
oauth2.grant.post_comment.vote: Оценивать положительно, отрицательно и
продвигать комментарии записей.
oauth2.grant.post_comment.report: Сообщить о комментарии к посту.
oauth2.grant.user.all: Просмотреть и изменить ваш профиль, сообщения,
уведомления; просмотреть и изменить ваши права доступа в приложениях;
подписываться или блокировать других пользователей; просмотреть списки
пользователей на которых вы подписались, или блокировали.
oauth2.grant.user.profile.all: Просматривать и редактировать ваш профиль.
oauth2.grant.user.profile.read: Просматривать ваш профиль.
oauth2.grant.user.profile.edit: Редактировать ваш профиль.
oauth2.grant.user.message.all: Просматривать ваши сообщения и отправлять
сообщения другим пользователям.
oauth2.grant.user.message.read: Просматривать ваши сообщения.
oauth2.grant.user.message.create: Отправлять сообщения другим пользователям.
oauth2.grant.user.notification.all: Просматривать и очистить ваши уведомления.
oauth2.grant.user.notification.read: Просматривать ваши уведомления, включая
уведомления о сообщениях.
oauth2.grant.user.notification.delete: Очищать ваши уведомления.
oauth2.grant.user.oauth_clients.all: Просматривать и изменять разрешения,
которые вы предоставили другим приложениям OAuth2.
oauth2.grant.user.oauth_clients.read: Просматривать разрешения, предоставленные
другим приложениям OAuth2.
oauth2.grant.user.oauth_clients.edit: Изменять разрешения, предоставленные
другим приложениям OAuth2.
oauth2.grant.user.follow: Подписываться на пользователей и отписываться от них,
просматривать список пользователей, на которых вы подписаны.
oauth2.grant.user.block: Блокировать и разблокировать пользователей,
просматривать ваш список заблокированных пользователей.
oauth2.grant.moderate.all: Выполнить любое действие модерации, на которые у вас
есть разрешения, в модерируемом журнале.
oauth2.grant.moderate.entry.all: Модерировать ветви в журналах, которые вы
модерируете.
oauth2.grant.moderate.entry.change_language: Изменять языки веток в журналах,
которые вы модерируете.
oauth2.grant.moderate.entry.pin: Закреплять ветви наверху журналов, которые вы
модерируете.
oauth2.grant.moderate.entry.set_adult: Помечать ветви как NSFW в журналах,
которые вы модерируете.
oauth2.grant.moderate.entry.trash: Удалять и восстанавливать ветви в журналах,
которые вы модерируете.
oauth2.grant.moderate.entry_comment.all: Модерировать комментарии в ветвях
журналов, которые вы модерируете.
oauth2.grant.moderate.entry_comment.change_language: Изменять язык комментариев
в ветвях журналов, которые вы модерируете.
oauth2.grant.moderate.entry_comment.set_adult: Помечать комментарии NSFW в
ветвях журналов, которые вы модерируете.
oauth2.grant.moderate.entry_comment.trash: Удалять и восстанавливать комментарии
в ветвях журналов, которые вы модерируете.
oauth2.grant.moderate.post.all: Модерировать записи в журналах, которые вы
модерируете.
oauth2.grant.moderate.post.change_language: Изменять язык записей в журналах,
которые вы модерируете.
oauth2.grant.moderate.post.set_adult: Помечать записи как NSFW в журналах,
которые вы модерируете.
oauth2.grant.moderate.post.trash: Удалять и восстанавливать записи в журналах,
которые вы модерируете.
oauth2.grant.moderate.post_comment.all: Модерировать комментарии под записями
журналов, которые вы модерируете.
oauth2.grant.moderate.post_comment.change_language: Изменять язык комментариев
под записями в журналах, которые вы модерируете.
oauth2.grant.moderate.post_comment.set_adult: Помечать как NSFW комментарии под
записями в журналах, которые вы модерируете.
oauth2.grant.moderate.post_comment.trash: Удалять и восстанавливать комментарии
под записями в журналах, которые вы модерируете.
oauth2.grant.moderate.magazine.all: Управлять банами, отчётами и просматривать
удалённые элементы в вашем модерируемом журнале.
oauth2.grant.moderate.magazine.ban.all: Управлять забаненными полльзователями в
вашем модерируемом журнале.
oauth2.grant.moderate.magazine.ban.read: просмотреть забанненых полльзователей в
вашем модерируемом журнале.
oauth2.grant.moderate.magazine.ban.create: Банить пользователей в вашем
модерируемом журнале.
oauth2.grant.admin.entry_comment.purge: полностью удалить любой комментарий в
ветке вашего инстанса.
oauth2.grant.admin.post.purge: Полностью удалить любой пост в вашем инстансе.
oauth2.grant.admin.post_comment.purge: Полностью удалить любой комментарий к
посту в вашем инстансе.
oauth2.grant.admin.magazine.all: Перемещать ветки между журналами или полностью
удалять их в вашем инстансе.
oauth2.grant.admin.magazine.move_entry: Перемещать ветки между журналами вашего
инстанса.
oauth2.grant.admin.magazine.purge: Полностью удалить журнал в вашем инстансе.
oauth2.grant.admin.user.all: Банить, проверить, полностью удалить пользователей
вашего инстанса.
oauth2.grant.admin.user.ban: Банить или разбанить пользователей вашего инстанса.
oauth2.grant.admin.user.verify: Проверить пользователей вашего инстанса.
oauth2.grant.admin.user.delete: Удалить пользователей вашего инстанса.
oauth2.grant.admin.user.purge: Полностью удалить пользователей вашего инстанса.
oauth2.grant.admin.instance.all: Посмотеть и обновить настройки или инфрмацию
инстанса.
oauth2.grant.admin.instance.stats: Посмотреть статус вашего инстанса.
oauth2.grant.admin.instance.settings.all: Посмотреть и обновить настройки вашего
инстанса.
oauth2.grant.admin.instance.settings.read: Посмотреть настройки вашего инстанса.
oauth2.grant.admin.instance.settings.edit: Обновить настройки вашего инстанса.
oauth2.grant.admin.instance.information.edit: Обновите О, ЧаВо, Контакты, Сервис
и поддержка, А также старницу Политики Конфиденциальности в вашем инстансе.
oauth2.grant.admin.federation.all: Посмотреть и обновить текущую дефедерацию
инстанса.
oauth2.grant.admin.federation.read: Посмотреть список федерации инстанса.
oauth2.grant.admin.federation.update: Добавить или удалить инстанс в списке
дефедерации инстансов.
oauth2.grant.admin.oauth_clients.all: Посмотреть или отозвать существующих
клиентов OAuth2 в вашем инстансе.
oauth2.grant.admin.oauth_clients.read: Посмотреть существующих клиентов OAuth2 в
вашем инстасне и статистику их использования.
oauth2.grant.admin.oauth_clients.revoke: Отозвать доступ клиентов OAuth2 в вашем
инстансе.
last_active: Последняя активность
flash_post_pin_success: Пост успешно закреплён.
flash_post_unpin_success: Пост успешно откреплён.
comment_reply_position_help: Отображать ответ на комментарий вверху или внизу
страницы. Когда включена бесконечная прокрутка, позиция всегда будет
отображаться вверху.
show_avatars_on_comments: Показать аватары комментаторов
single_settings: Отдельные настройки
update_comment: Обновить комментарий
show_avatars_on_comments_help: Показать или скрыть автары пользователя во время
просмотра отдельных веток или постов.
comment_reply_position: Позиция ответа на комментарий
magazine_theme_appearance_custom_css: Пользовательский CSS будет использоваться
при просмотре контента в вашем журнале.
magazine_theme_appearance_icon: Пользовательская иконка журнала не выбрана.
Будет выбрана и использована стандартная иконка.
magazine_theme_appearance_background_image: Пользовательское фоновое изображение
будет использовано при просмотре контента вашего журнала.
moderation.report.approve_report_title: Утвердить отчёт
moderation.report.reject_report_title: Отклонитьотчёт
moderation.report.ban_user_description: Вы хотите забанить полльзователя
(%username%) который создаёт контент в этом журнале??
moderation.report.approve_report_confirmation: Вы уверены, что хотите
подтвердить этот отчёт?
subject_reported_exists: Об этом контенте уже сообщалось..
moderation.report.ban_user_title: Заблокировать пользователя
moderation.report.reject_report_confirmation: Вы уверены, что хотите отозвать
этот отчёт?
oauth2.grant.moderate.post.pin: Закрепить пост в топе вашего модерируемого
журнала.
delete_content: Удалить контент
purge_content: Очистить контент
delete_content_desc: Удалить контент полльзователся, оставив при этом ответы
других полльзователей в созданных ветках,сообщениях и комментариях.
purge_content_desc: Полная очистка контента полльзователя, включая удаление
ответов других пользователей в созданных ветках, постах и комментариях.
delete_account_desc: Удалить учётную запись, включая ответы других пользователей
в созданных темах, сообщениях и комментариях.
two_factor_authentication: Двухфакторная аутентификация
two_factor_backup: Коды восстановления двухфакторной аутентификации
2fa.authentication_code.label: Код аутентификации
2fa.verify: Подтверждение
2fa.code_invalid: Код аутентификации не действителен
2fa.setup_error: Ошибка подключения 2FA аккаунта
2fa.enable: Установка двухфакторной аутентификации
2fa.disable: Отключение двухфакторной аутентификации
2fa.backup: Ваши коды восстановления 2FA
2fa.backup-create.help: Вы можете создать новые коды восстановления
аутентификации; это приведёт к отмене существующего кода.
2fa.backup-create.label: Создать новый код восстановления аутентификации
2fa.remove: Удалить 2FA
2fa.add: Добавить мой аккаунт
2fa.verify_authentication_code.label: Введите двухфакторный код для проверки
настройки
2fa.qr_code_img.alt: QR-код позволяющий настроить двухфакторную аутентификацию
для вашего аккаунта
2fa.qr_code_link.title: Переход по этой ссылке может позволить вашей платформе
заригистрировать этк двухфакторную аутентификацию
2fa.user_active_tfa.title: 2ФА активна
2fa.available_apps: Используйте приложение для двухфакторной аутентификации,
например %google_authenticator%, %aegis% (Android) или %raivo% (iOS) для
сканирования QR-кода.
2fa.backup_codes.help: вы можете использовать эти коды. если у вас нет вашего
устройства или приложения. Коды будут показаны снова , вы
сможете использовать каждый из них только один раз .
2fa.backup_codes.recommendation: Рекомендуем сохранить эту заметку в безопасное
место.
cancel: Отмена
password_and_2fa: Пароль и 2ФА
flash_account_settings_changed: Настройки вашего аккаунта успешно изменены. Вам
нужно войти ещё раз.
show_subscriptions: Показать подписки
subscription_sort: Сортировать.
alphabetically: По алфавиту
subscriptions_in_own_sidebar: В отдельной боковой панели
sidebars_same_side: боковая панель на той же стороне
subscription_sidebar_pop_out_right: Переместить отдельную боковую панель вправо
subscription_sidebar_pop_out_left: Переместить отдельную боковую панель на лево
subscription_sidebar_pop_in: Переместить подписки во встроенную панель
subscription_panel_large: Большая панель
subscription_header: Подписки на журналы
close: Звкрыто
position_bottom: Нижний
position_top: Верхний.
pending: В ожидании
flash_thread_new_error: Ветка не может быть создана. Что-то пошло не так.
flash_email_was_sent: Электронное письмо успешно было отправлено.
flash_email_failed_to_sent: Электронное письмо не может быть отправлено.
flash_post_new_success: Пост успешно создан.
flash_post_new_error: Пост не может быть создан. Что-то пошло не так.
flash_magazine_theme_changed_success: Успешное обновление внешнего вида журнала.
flash_magazine_theme_changed_error: Неудачное обновление внешнего вида журнала.
flash_comment_new_success: Комментарий успешно создан.
flash_comment_edit_success: Комментарий успешно обновлён.
flash_comment_new_error: Комментарий не создан. Что-то пошло не так.
flash_comment_edit_error: Неудачное редактироване комментария. Что-то пошло не
так.
flash_user_settings_general_success: Настройки пользователя успешно сохранены.
flash_user_settings_general_error: ошибка сохранения настроек пользователя.
flash_user_edit_profile_error: ошибка сохранения настроек профиля.
flash_user_edit_profile_success: Настройки профиля пользователя успешно
сохранены.
flash_user_edit_email_error: Ошибка изменения адреса электронной почты.
flash_user_edit_password_error: Ошибка изменения пароля.
flash_thread_edit_error: Ошибка изменения ветки. Что-то пошло не так.
flash_post_edit_error: Ошибка редактирования поста. Что-то пошло не так.
flash_post_edit_success: Пост был успешно отредактирован.
page_width: Ширина страницы
page_width_max: максимальная.
page_width_auto: Автоматически.
page_width_fixed: Фиксированная
open_url_to_fediverse: Открыть оригинальный URL
change_my_avatar: Изменить мой аватар
change_my_cover: Изменить мою обложку
edit_my_profile: Редактировать мой профиль
account_settings_changed: Настройки вашего аккаунта успешно изменены. Вам нужно
войти ещё раз.
magazine_deletion: Удалённый журнала
delete_magazine: Удаление журнала
restore_magazine: Восстановление журнала
purge_magazine: Очистить журнал
magazine_is_deleted: Журнал удалёен. Вы можете
восстановить егов течениие 30 дней.
suspend_account: Заблокировать аккаунт
unsuspend_account: Разблокировать account
account_suspended: Аккаунт был заблокирован.
account_unsuspended: Аккаунт был разблокирован.
deletion: Удаление
user_suspend_desc: Приостановить ваш аккаунт и скрыть контент в инстансе не
удаляя навсегда, вы сможете восстановить его через какое то время.
account_banned: Аккаунт был забанен.
account_unbanned: Аккаунт был разбанен.
account_is_suspended: Аккаунт пользователя приостановлен.
remove_following: Удалить фоловеров
remove_subscriptions: Удалить подписчиков
apply_for_moderator: Применить для модератора
request_magazine_ownership: Запрос права собственности на журнал
cancel_request: Отменить запрос
abandoned: Заброшенный
ownership_requests: Запрос на владение
accept: Принять
moderator_requests: Запрос мода
action: Действие
user_badge_op: ОР
user_badge_admin: Администратор
user_badge_global_moderator: Глобальный мод
user_badge_moderator: Мод.
user_badge_bot: Бот.
announcement: Объявление
keywords: Ключевые слова
deleted_by_moderator: Ветка, пост или комментарий были удалены модератором
deleted_by_author: Ветка, пост или комментарий были удалены автором
sensitive_warning: Деликатный контент
sensitive_toggle: Изменить видимость конфиденциального контента
sensitive_show: Нажать чтобы посмотреть
sensitive_hide: Нажать чтобы скрыть
all_time: Всё время
subscribers_count: '{0}Подписки|{1}Подписка|]1,Inf[ Подписок'
menu: Меню
followers_count: '{0}Подписчиков|{1}Подписчик|]1,Inf[ Подписчиков'
remove_media: Удалить медиа
details: Детали
spoiler: Спойлер
private_instance: Требовать авторизацию для просмотра содержимого сервера
cake_day: День варенья
from: от
sort_by: Упорядочивание
hidden: Скрыты
disabled: Выключены
test_push_message: Привет, мир!
notification_title_new_post: Новая запись
downvotes_mode: Режим отрицательных оценок
enabled: Включены
tag: Метка
edit_entry: Редактировать ветку
unban: Разблокировать
ban_hashtag_btn: Заблокировать хештег
toolbar.spoiler: Спойлер
federation_page_dead_title: Мёртвые серверы
account_deletion_title: Удаление аккаунта
notification_title_new_thread: Новая ветвь
marked_for_deletion_at: Помечено для удаления %date%
marked_for_deletion: Помечено для удаления
direct_message: Личное сообщение
subscribe_for_updates: Подпишитесь, чтобы получать оповещения о новых
публикациях.
account_deletion_button: Удалить учётную запись
account_deletion_immediate: Удалить сейчас
notification_title_new_reply: Новый ответ
bookmark_add_to_list: Добавить закладку в %list%
bookmark_remove_from_list: Удалить закладку из %list%
max_image_size: Макс. размер файла
bookmark_lists: Списки закладок
bookmarks: Закладки
bookmark_list_make_default: Сделать основным
bookmark_add_to_default_list: Добавить закладку в основной список
table_of_contents: Содержимое
bookmark_remove_all: Удалить все закладки
signup_requests_paragraph: Эти пользователи желают зарегистрироваться на вашем
сервере. Они не могут входить в учётные записи, пока вы не одобрите их
запросы.
comment_not_found: Комментарий не найден
count: Количество
bookmark_list_create: Создать
bookmark_list_selected_list: Выбранный список
search_type_all: Ветки и микроблоги
search_type_entry: Ветки
select_user: Выбор пользователя
show_magazine_domains: Показывать домены журналов
show_user_domains: Показывать домены уч. записей
is_default: Основной
bookmark_list_is_default: Основной список
bookmarks_list: Закладки из %list%
signup_requests_header: Запрошенные регистрации
email_verification_pending: Для входа требуется подтверждение адреса почты.
email_application_pending: Для входа требуется одобрение регистрации
администратором.
email_application_approved_title: Ваша регистрация одобрена
email_application_rejected_title: Ваш запрос регистрации отклонён
edited: изменено
remove_user_cover: Убрать шапку
related_entry: Связанное
sso_show_first: Сперва предлагать SSO на страницах регистрации и входа
remove_user_avatar: Убрать изображение профиля
change_downvotes_mode: Сменить режим отрицательного оценивания
viewing_one_signup_request: Вы просматриваете запрос регистрации %username%
reporting_user: Доносчик
own_report_rejected: Ваша жалоба была отклонена
own_report_accepted: Ваша жалоба была принята
sso_registrations_enabled: Регистрации через SSO включены
sso_only_mode: Разрешить регистрации и вход только через SSO
reported_user: Жалоба на
show: Показать
hide: Скрыть
remove_schedule_delete_account_desc: Всё содержимое будет снова доступно и
пользователь снова сможет входить в учётную запись.
restrict_magazine_creation: Разрешить создание локальных журналов только
администраторам и модераторам сервера
continue_with: Продолжить с
notify_on_user_signup: Новые регистрации
flash_image_download_too_large_error: Невозможно добавить изображение, т.к. оно
слишком большое (макс. размер %bytes%)
flash_thread_tag_banned_error: Невозможно создать ветвь, т.к. содержимое
запрещено.
sso_registrations_enabled.error: Создание новых уч. записей через сторонние
системы идентификации сейчас недоступно.
report_accepted: Жалоба была принята
own_content_reported_accepted: Жалоба на содержимое, которую вы создали, была
рассмотрена и принята.
open_report: Открыть жалобу
remove_schedule_delete_account: Отменить запланированное удаление
notification_title_message: Новое личное сообщение
magazine_posting_restricted_to_mods: Разрешить создание ветвей только
модераторам
new_magazine_description: Новый журнал (активен менее чем %days% дней)
admin_users_inactive: Неактивен
new_user_description: Новый пользователь (активен менее чем %days% дней)
admin_users_active: Активен
notification_title_new_comment: Новый комментарий
user_verify: Активировать уч. запись
notification_title_edited_comment: Комментарий отредактирован
notification_title_mention: Вас упомянули
unregister_push_notifications_button: Удалить регистрацию пуш-уведомлений
test_push_notifications_button: Проверить уведомления
notification_title_removed_comment: Комментарий удалён
version: Версия
last_failed_contact: Последнее неуспешное взаимодействие
admin_users_suspended: Приостановлен
admin_users_banned: Заблокирован
bookmark_list_create_placeholder: введите название...
bookmark_list_create_label: Название списка
bookmarks_list_edit: Ред. список заметок
bookmark_list_edit: Редактировать
search_type_post: Микроблоги
oauth2.grant.user.bookmark.remove: Удалять заметки
oauth2.grant.user.bookmark_list: Просматривать, изменять и удалять ваши списки
заметок
oauth2.grant.user.bookmark.add: Добавлять заметки
notification_body_new_signup: Зарегистрировался пользователь %u%.
answered: отвечен
notification_title_ban: Вы заблокированы
notification_title_edited_post: Запись отредактирована
oauth2.grant.user.bookmark_list.delete: Удалять ваши списки заметок
oauth2.grant.user.bookmark: Добавлять и удалять заметки
oauth2.grant.user.bookmark_list.edit: Изменять ваши списки заметок
last_successful_deliver: Последняя успешная отправка
notification_title_edited_thread: Ветвь была отредактирована
2fa.manual_code_hint: Если не получается сканировать QR-код, введите секретный
ключ вручную
oauth2.grant.user.bookmark_list.read: Просматривать ваши списки заметок
register_push_notifications_button: Зарегистрировать канал пуш-увдеомлений
notification_title_new_signup: Пользователь зарегистрировался
notification_body2_new_signup_approval: Необходимо утвердить запрос регистрации,
прежде, чем пользователь сможет войти
last_successful_receive: Последнее успешное получение
schedule_delete_account_desc: Запланировать удаление этой учётной записи через
30 дней. Пользователь и все его публикации будут скрыты, и он не сможет
входить в учётную запись.
signup_requests: Запросы на регистрацию
notification_title_removed_thread: Ветвь была удалена
notification_title_removed_post: Запись удалена
schedule_delete_account: Запланировать удаление
toolbar.emoji: Эмодзи
application_text: Расскажите, почему вы хотите присоединиться
show_rich_mention_help: Отображать имена и изображения профилей пользователей,
упомянутых в тексте.
show_rich_mention: Расширенные упоминания
show_rich_mention_magazine: Расширенные упоминания журналов
show_rich_mention_magazine_help: Отображать названия и изображения журналов,
упомянутых в тексте.
show_rich_ap_link: Расширенные ссылки AP
and: и
================================================
FILE: translations/messages.sv.yaml
================================================
{}
================================================
FILE: translations/messages.ta.yaml
================================================
sort_by: வரிசைப்படுத்தவும்
top: மேலே
empty: காலி
follow: பின்தொடர்
unfollow: பின்தொடரவும்
reply: பதில்
to: பெறுநர்
from: இருந்து
username: பயனர்பெயர்
email: மின்னஞ்சல்
related_tags: தொடர்புடைய குறிச்சொற்கள்
go_to_content: உள்ளடக்கத்திற்குச் செல்லுங்கள்
go_to_filters: வடிப்பான்களுக்குச் செல்லுங்கள்
logout: விடுபதிகை
6h: தாஆ
12h: 12 ம
1d: 1 டி
1w: 1W
1m: 1 மீ
general: பொது
profile: சுயவிவரம்
reports: அறிக்கைகள்
notifications: அறிவிப்புகள்
messages: செய்திகள்
appearance: தோற்றம்
homepage: முகப்புப்பக்கம்
save: சேமி
default_theme: இயல்புநிலை கருப்பொருள்
flash_register_success: கப்பலில் வரவேற்கிறோம்! உங்கள் கணக்கு இப்போது பதிவு
செய்யப்பட்டுள்ளது. ஒரு கடைசி படி - உங்கள் கணக்கை உயிர்ப்பிக்கும்
செயல்படுத்தும் இணைப்பிற்கு உங்கள் இன்பாக்சைச் சரிபார்க்கவும்.
added_new_post: புதிய இடுகையைச் சேர்த்தது
purge: தூய்மைப்படுத்துதல்
right: வலது
federation: கூட்டமைப்பு
status: நிலை
on: ஆன்
unban: முணுமுணுப்பு
ban_hashtag_btn: தடை ஏச்டேக்
unban_hashtag_btn: ஐட்
filters: வடிப்பான்கள்
bans: தடைகள்
done: முடிந்தது
unban_account: கட்டுப்பாடற்ற கணக்கு
sidebar: பக்கப்பட்டி
account_deletion_button: கணக்கை நீக்கு
errors.server500.title: 500 உள் சேவையக பிழை
delete_content: உள்ளடக்கத்தை நீக்கு
two_factor_authentication: இரண்டு காரணி ஏற்பு
2fa.setup_error: கணக்கிற்கு 2FA ஐ இயக்குவதில் பிழை
2fa.enable: இரண்டு காரணி அங்கீகாரத்தை அமைக்கவும்
cancel: ரத்துசெய்
admin_users_banned: தடைசெய்யப்பட்டது
user_verify: கணக்கைச் செயல்படுத்தவும்
max_image_size: அதிகபட்ச கோப்பு அளவு
comment_not_found: கருத்து கிடைக்கவில்லை
type.link: இணைப்பு
type.article: நூல்
type.photo: புகைப்படம்
type.video: ஒளிதோற்றம்
type.smart_contract: அறிவுள்ள ஒப்பந்தம்
type.magazine: செய்தித் தாள்
thread: நூல்
threads: நூல்கள்
microblog: மைக்ரோ பிளாக்
people: மக்கள்
events: நிகழ்வுகள்
magazine: செய்தித் தாள்
magazines: பத்திரிகைகள்
search: தேடல்
add: கூட்டு
select_channel: ஒரு சேனலைத் தேர்ந்தெடுக்கவும்
login: புகுபதிகை
marked_for_deletion: நீக்குவதற்கு குறிக்கப்பட்டுள்ளது
marked_for_deletion_at: '%தேதி %இல் நீக்கப்படுவதற்கு குறிக்கப்பட்டுள்ளது'
favourites: மேம்பாடுகள்
favourite: பிடித்த
more: மேலும்
avatar: அவதார்
hot: சூடான
active: செயலில்
newest: புதியது
oldest: பழமையானது
commented: கருத்து
change_view: பார்வையை மாற்றவும்
filter_by_time: நேரம் மூலம் வடிகட்டவும்
filter_by_type: வகை மூலம் வடிகட்டவும்
filter_by_subscription: சந்தா மூலம் வடிகட்டவும்
filter_by_federation: கூட்டமைப்பு நிலை மூலம் வடிகட்டவும்
comments_count: '{0} கருத்துகள் | {1} கருத்து |] 1, Inf [கருத்துகள்'
subscribers_count: '{0} சந்தாதாரர்கள் | {1} சந்தாதாரர் |] 1, INF [சந்தாதாரர்கள்'
followers_count: '{0} பின்தொடர்பவர்கள் | {1} பின்தொடர்பவர் |] 1, INF [பின்தொடர்பவர்கள்'
added: சேர்க்கப்பட்டது
up_votes: ஊக்கங்கள்
down_votes: குறைக்கிறது
no_comments: கருத்துகள் இல்லை
created_at: உருவாக்கப்பட்டது
owner: உரிமையாளர்
subscribers: சந்தாதாரர்கள்
online: ஆன்லைனில்
comments: கருத்துகள்
posts: இடுகைகள்
replies: பதில்கள்
moderators: மதிப்பீட்டாளர்கள்
mod_log: மிதமான பதிவு
add_comment: கருத்து சேர்க்கவும்
add_post: இடுகையைச் சேர்க்கவும்
add_media: மீடியாவைச் சேர்க்கவும்
remove_media: மீடியாவை அகற்று
markdown_howto: ஆசிரியர் எவ்வாறு செயல்படுகிறார்?
enter_your_comment: உங்கள் கருத்தை உள்ளிடவும்
enter_your_post: உங்கள் இடுகையை உள்ளிடவும்
activity: செய்கைப்பாடு
cover: கவர்
related_posts: தொடர்புடைய இடுகைகள்
random_posts: சீரற்ற பதிவுகள்
federated_magazine_info: இந்த செய்தித் தாள் ஒரு கூட்டாட்சி சேவையகத்திலிருந்து
வந்தது மற்றும் முழுமையடையாது.
disconnected_magazine_info: இந்த செய்தித் தாள் புதுப்பிப்புகளைப் பெறவில்லை
(கடைசி செயல்பாடு % நாட்கள் % நாள் (கள்) முன்பு).
always_disconnected_magazine_info: இந்த செய்தித் தாள் புதுப்பிப்புகளைப்
பெறவில்லை.
subscribe_for_updates: புதுப்பிப்புகளைப் பெறத் தொடங்க குழுசேரவும்.
federated_user_info: இந்த சுயவிவரம் கூட்டாட்சி சேவையகத்திலிருந்து வந்தது மற்றும்
முழுமையடையாமல் இருக்கலாம்.
go_to_original_instance: தொலைநிலை நிகழ்வைக் காண்க
subscribe: குழுசேர்
unsubscribe: குழுவிலகவும்
login_or_email: உள்நுழைவு அல்லது மின்னஞ்சல்
password: கடவுச்சொல்
remember_me: என்னை நினைவில் கொள்ளுங்கள்
dont_have_account: கணக்கு இல்லையா?
you_cant_login: உங்கள் கடவுச்சொல்லை மறந்துவிட்டீர்களா?
already_have_account: ஏற்கனவே ஒரு கணக்கு இருக்கிறதா?
register: பதிவு செய்யுங்கள்
reset_password: கடவுச்சொல்லை மீட்டமைக்கவும்
downvotes_mode: டவுன்வோட்ச் பயன்முறை
change_downvotes_mode: டவுன்வோட்ச் பயன்முறையை மாற்றவும்
disabled: முடக்கப்பட்டது
hidden: மறைக்கப்பட்ட
enabled: இயக்கப்பட்டது
useful: பயனுள்ள
show_more: மேலும் காட்டு
in: இல்
repeat_password: கடவுச்சொல்லை மீண்டும் செய்யவும்
agree_terms: '%விதிமுறைகளுக்கு ஒப்புதல்_LINK_START%விதிமுறைகள் மற்றும் நிபந்தனைகள்%விதிமுறைகள்_லின்க்_எண்ட்%மற்றும்%பாலிசி_லின்க்_ச்டார்ட்%தனியுரிமைக்
கொள்கை%பாலிசி_லின்க்_எண்ட்%'
terms: பணி விதிமுறைகள்
privacy_policy: தனியுரிமைக் கொள்கை
about_instance: பற்றி
all_magazines: அனைத்து பத்திரிகைகளும்
stats: புள்ளிவிவரங்கள்
fediverse: ஃபெடிவர்ச்
create_new_magazine: புதிய பத்திரிகையை உருவாக்கவும்
add_new_article: புதிய நூலைச் சேர்க்கவும்
add_new_link: புதிய இணைப்பைச் சேர்க்கவும்
add_new_photo: புதிய புகைப்படத்தைச் சேர்க்கவும்
add_new_post: புதிய இடுகையைச் சேர்க்கவும்
add_new_video: புதிய வீடியோவைச் சேர்க்கவும்
contact: தொடர்பு
faq: கேள்விகள்
rss: ஆர்.எச்.எச்
change_theme: கருப்பொருள் மாற்றவும்
help: உதவி
check_email: உங்கள் மின்னஞ்சலை சரிபார்க்கவும்
reset_check_email_desc: உங்கள் மின்னஞ்சல் முகவரியுடன் ஏற்கனவே ஒரு கணக்கு
இருந்தால், உங்கள் கடவுச்சொல்லை மீட்டமைக்க நீங்கள் பயன்படுத்தக்கூடிய இணைப்பைக்
கொண்ட மின்னஞ்சலைப் பெற வேண்டும். இந்த இணைப்பு %காலாவதியாகும்.
image_alt: பட மாற்று உரை
name: பெயர்
description: விவரம்
rules: விதிகள்
domain: டொமைன்
followers: பின்தொடர்பவர்கள்
following: பின்வருமாறு
reset_check_email_desc2: நீங்கள் மின்னஞ்சல் பெறவில்லை என்றால் உங்கள் ச்பேம்
கோப்புறையை சரிபார்க்கவும்.
try_again: மீண்டும் முயற்சிக்கவும்
up_vote: பூச்ட்
down_vote: குறைக்க
email_confirm_header: வணக்கம்! உங்கள் மின்னஞ்சல் முகவரியை உறுதிப்படுத்தவும்.
email_confirm_content: 'உங்கள் MBIN கணக்கை செயல்படுத்த தயாரா? கீழே உள்ள இணைப்பைக்
சொடுக்கு செய்க:'
email_verify: மின்னஞ்சல் முகவரியை உறுதிப்படுத்தவும்
email_confirm_expire: இணைப்பு ஒரு மணி நேரத்தில் காலாவதியாகும் என்பதை நினைவில்
கொள்க.
email_confirm_title: உங்கள் மின்னஞ்சல் முகவரியை உறுதிப்படுத்தவும்.
select_magazine: ஒரு பத்திரிகையைத் தேர்ந்தெடுக்கவும்
add_new: புதியதைச் சேர்க்கவும்
url: முகவரி
title: தலைப்பு
body: உடல்
tags: குறிச்சொற்கள்
tag: குறிச்சொல்
badges: பேட்ச்கள்
is_adult: 18+ / NSFW
eng: இன்சி
oc: OC
image: படம்
subscriptions: சந்தாக்கள்
overview: கண்ணோட்டம்
cards: அட்டைகள்
chat_view: அரட்டை காட்சி
tree_view: மரக் காட்சி
table_view: அட்டவணை பார்வை
cards_view: அட்டைகள் பார்வை
3h: 3x
1y: 1y
links: இணைப்புகள்
articles: நூல்கள்
photos: புகைப்படங்கள்
videos: வீடியோக்கள்
report: அறிக்கை
share: பங்கு
copy_url: MIN முகவரி ஐ நகலெடுக்கவும்
copy_url_to_fediverse: அசல் முகவரி ஐ நகலெடுக்கவும்
share_on_fediverse: ஃபெடிவர்சில் பங்கு
edit: தொகு
are_you_sure: நீங்கள் உறுதியாக இருக்கிறீர்களா?
moderate: மிதமான
reason: காரணம்
edit_entry: நூல் திருத்து
columns: நெடுவரிசைகள்
user: பயனர்
joined: இணைந்தது
moderated: மிதமான
people_local: உள்ளக
people_federated: கூட்டாட்சி
reputation_points: நற்பெயர் புள்ளிகள்
go_to_search: தேடச் செல்லவும்
subscribed: சந்தா
all: அனைத்தும்
classic_view: கிளாசிக் பார்வை
compact_view: சிறிய பார்வை
delete: நீக்கு
edit_post: இடுகையைத் திருத்து
edit_comment: மாற்றங்களைச் சேமிக்கவும்
menu: பட்டியல்
settings: அமைப்புகள்
blocked: தடுக்கப்பட்டது
hide_adult: NSFW உள்ளடக்கத்தை மறைக்கவும்
featured_magazines: சிறப்பு பத்திரிகைகள்
show_profile_subscriptions: செய்தித் தாள் சந்தாக்களைக் காட்டு
show_profile_followings: பின்வரும் பயனர்களைக் காட்டு
notify_on_new_entry_reply: நான் எழுதிய நூல்களில் எந்த நிலை கருத்துகளும்
notify_on_new_entry_comment_reply: எந்த நூல்களிலும் எனது கருத்துக்களுக்கான
பதில்கள்
notify_on_new_post_reply: நான் எழுதிய இடுகைகளுக்கு எந்த நிலை பதில்களும்
notify_on_new_post_comment_reply: எந்தவொரு இடுகைகளிலும் எனது கருத்துக்களுக்கான
பதில்கள்
privacy: தனியுரிமை
notify_on_new_entry: நான் சந்தா செலுத்திய எந்த பத்திரிகையிலும் புதிய நூல்கள்
(இணைப்புகள் அல்லது கட்டுரைகள்)
notify_on_new_posts: நான் சந்தா செலுத்திய எந்த பத்திரிகையிலும் புதிய இடுகைகள்
notify_on_user_signup: புதிய கையொப்பங்கள்
light: ஒளி
solarized_light: சோலரிச் லைட்
solarized_dark: சோலரிச் இருண்ட
default_theme_auto: ஒளி/இருண்ட (ஆட்டோ கண்டறிதல்)
solarized_auto: சோலரிச் (ஆட்டோ கண்டறிதல்)
font_size: எழுத்துரு அளவு
size: அளவு
about: பற்றி
old_email: தற்போதைய மின்னஞ்சல்
new_email: புதிய மின்னஞ்சல்
new_email_repeat: புதிய மின்னஞ்சலை உறுதிப்படுத்தவும்
current_password: தற்போதைய கடவுச்சொல்
new_password: புதிய கடவுச்சொல்
new_password_repeat: புதிய கடவுச்சொல்லை உறுதிப்படுத்தவும்
change_email: மின்னஞ்சலை மாற்றவும்
change_password: கடவுச்சொல்லை மாற்றவும்
expand: விரிவாக்கு
collapse: சரிவு
domains: களங்கள்
error: பிழை
votes: வாக்குகள்
theme: கருப்பொருள்
dark: இருண்ட
boosts: ஊக்கங்கள்
show_users_avatars: பயனர்களின் அவதாரங்களைக் காட்டு
yes: ஆம்
no: இல்லை
show_thumbnails: சிறு உருவங்களைக் காட்டு
rounded_edges: வட்ட விளிம்புகள்
removed_thread_by: ஒரு நூலை அகற்றிவிட்டது
restored_thread_by: ஒரு நூலை மீட்டெடுத்துள்ளார்
show_magazines_icons: பத்திரிகைகளின் சின்னங்களைக் காட்டு
removed_comment_by: ஒரு கருத்தை நீக்கிவிட்டார்
restored_comment_by: கருத்தை மீட்டெடுத்துள்ளார்
removed_post_by: ஒரு இடுகையை அகற்றியுள்ளது
flash_magazine_edit_success: செய்தித் தாள் வெற்றிகரமாக திருத்தப்பட்டுள்ளது.
flash_mark_as_adult_success: இந்த இடுகை வெற்றிகரமாக NSFW என குறிக்கப்பட்டுள்ளது.
flash_unmark_as_adult_success: இந்த இடுகை வெற்றிகரமாக NSFW என குறிக்கப்படவில்லை.
restored_post_by: ஒரு இடுகையை மீட்டெடுத்துள்ளார்
he_banned: தடை
he_unbanned: முணுமுணுப்பு
read_all: அனைத்தையும் படியுங்கள்
show_all: அனைத்தையும் காட்டு
flash_thread_new_success: நூல் வெற்றிகரமாக உருவாக்கப்பட்டுள்ளது, இப்போது மற்ற
பயனர்களுக்குத் தெரியும்.
flash_thread_edit_success: நூல் வெற்றிகரமாக திருத்தப்பட்டுள்ளது.
flash_thread_delete_success: நூல் வெற்றிகரமாக நீக்கப்பட்டுள்ளது.
flash_thread_pin_success: நூல் வெற்றிகரமாக பொருத்தப்பட்டுள்ளது.
flash_thread_unpin_success: நூல் வெற்றிகரமாக இணைக்கப்படவில்லை.
flash_magazine_new_success: செய்தித் தாள் வெற்றிகரமாக உருவாக்கப்பட்டுள்ளது.
நீங்கள் இப்போது புதிய உள்ளடக்கத்தைச் சேர்க்கலாம் அல்லது பத்திரிகையின்
நிர்வாகக் குழுவை ஆராயலாம்.
too_many_requests: வரம்பு மீறியது, தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும்.
set_magazines_bar: பத்திரிகைகள் பட்டி
mod_log_alert: எச்சரிக்கை - மதிப்பீட்டாளர்களால் அகற்றப்பட்ட விரும்பத்தகாத அல்லது
துன்பகரமான உள்ளடக்கம் மோட்லாக் இருக்கலாம். தயவுசெய்து எச்சரிக்கையுடன்
உடற்பயிற்சி செய்யுங்கள்.
added_new_thread: புதிய நூல் சேர்க்கப்பட்டது
edited_thread: ஒரு நூல் திருத்தப்பட்டது
mod_remove_your_thread: ஒரு மதிப்பீட்டாளர் உங்கள் நூலை அகற்றினார்
added_new_comment: புதிய கருத்தைச் சேர்த்தது
edited_comment: ஒரு கருத்தைத் திருத்தியுள்ளார்
set_magazines_bar_desc: கமாவுக்குப் பிறகு செய்தித் தாள் பெயர்களைச் சேர்க்கவும்
set_magazines_bar_empty_desc: புலம் காலியாக இருந்தால், செயலில் உள்ள பத்திரிகைகள்
பட்டியில் காட்டப்படும்.
replied_to_your_comment: உங்கள் கருத்துக்கு பதிலளித்தது
mod_deleted_your_comment: ஒரு மதிப்பீட்டாளர் உங்கள் கருத்தை நீக்கிவிட்டார்
edited_post: ஒரு இடுகையைத் திருத்தியுள்ளார்
mod_remove_your_post: ஒரு மதிப்பீட்டாளர் உங்கள் இடுகையை அகற்றினார்
added_new_reply: புதிய பதிலைச் சேர்த்தது
wrote_message: ஒரு செய்தி எழுதினார்
banned: உங்களுக்கு தடை விதித்தது
removed: மோட் மூலம் அகற்றப்பட்டது
comment: கருத்து
post: இடுகை
deleted: ஆசிரியரால் நீக்கப்பட்டது
mentioned_you: நீங்கள் குறிப்பிட்டுள்ளீர்கள்
ban_expired: தடை காலாவதியானது
send_message: நேரடி செய்தியை அனுப்பவும்
message: செய்தி
infinite_scroll: எல்லையற்ற ச்க்ரோலிங்
show_top_bar: மேல் பட்டியைக் காட்டு
sticky_navbar: ஒட்டும் நவ்பர்
subject_reported: உள்ளடக்கம் தெரிவிக்கப்பட்டுள்ளது.
sidebar_position: பக்கப்பட்டி நிலை
left: இடது
off: அணை
instances: நிகழ்வுகள்
upload_file: கோப்பைப் பதிவேற்றவும்
from_url: முகவரி இலிருந்து
magazine_panel: செய்தித் தாள் குழு
reject: நிராகரிக்கவும்
approve: ஒப்புதல்
ban: தடை
ban_hashtag_description: ஒரு ஏச்டேக்கைத் தடைசெய்வது இந்த ஏச்டேக்
உருவாக்கப்படுவதைத் தடுக்கும், அதே போல் ஏற்கனவே உள்ள இடுகைகளை இந்த ஏச்டேக்குடன்
மறைக்கும்.
unban_hashtag_description: ஒரு ஏச்டேக்கை தடைசெய்வது இந்த ஏச்டேக்குடன் மீண்டும்
இடுகைகளை உருவாக்க அனுமதிக்கும். இந்த ஏச்டேக்குடன் இருக்கும் இடுகைகள் இனி
மறைக்கப்படவில்லை.
approved: அங்கீகரிக்கப்பட்டது
rejected: நிராகரிக்கப்பட்டது
add_moderator: மதிப்பீட்டாளரைச் சேர்க்கவும்
add_badge: ஒட்டு சேர்க்கவும்
created: உருவாக்கப்பட்டது
expires: காலாவதியாகிறது
perm: நிரந்தர
expired_at: காலாவதியானது
add_ban: சேர்
trash: குப்பை
icon: படவுரு
pin: முள்
unpin: மூள்நீக்கு
change_magazine: பத்திரிகையை மாற்றவும்
change_language: மொழியை மாற்றவும்
mark_as_adult: மார்க் என்.எச்.எஃப்.டபிள்யூ
unmark_as_adult: NSFW UNCOLDER
change: மாற்றம்
pinned: பின்
preview: முன்னோட்டம்
firstname: முதல் பெயர்
send: அனுப்பு
active_users: செயலில் உள்ளவர்கள்
random_entries: சீரற்ற நூல்கள்
related_entries: தொடர்புடைய நூல்கள்
delete_account: கணக்கை நீக்கு
purge_account: கணக்கை தூய்மைப்படுத்துங்கள்
article: நூல்
reputation: நற்பெயர்
note: குறிப்பு
writing: எழுதுதல்
users: பயனர்கள்
content: உள்ளடக்கம்
week: வாரம்
weeks: வாரங்கள்
month: மாதம்
months: மாதங்கள்
year: ஆண்டு
federated: கூட்டாட்சி
local: உள்ளக
admin_panel: நிர்வாக குழு
dashboard: முகப்புப்பெட்டி
contact_email: மின்னஞ்சல் தொடர்பு
meta: மெட்டா
instance: சான்று
pages: பக்கங்கள்
FAQ: கேள்விகள்
type_search_term: தேடல் காலத்தைத் தட்டச்சு செய்க
federation_enabled: கூட்டமைப்பு இயக்கப்பட்டது
registrations_enabled: பதிவு இயக்கப்பட்டது
registration_disabled: பதிவு முடக்கப்பட்டது
restore: மீட்டமை
add_mentions_entries: குறிச்சொற்களை நூல்களில் சேர்க்கவும்
add_mentions_posts: இடுகைகளில் குறிப்பிடப்பட்ட குறிச்சொற்களைச் சேர்க்கவும்
Password is invalid: கடவுச்சொல் தவறானது.
Your account is not active: உங்கள் கணக்கு செயலில் இல்லை.
Your account has been banned: உங்கள் கணக்கு தடைசெய்யப்பட்டுள்ளது.
ban_account: கணக்கு தடை
related_magazines: தொடர்புடைய பத்திரிகைகள்
random_magazines: சீரற்ற பத்திரிகைகள்
magazine_panel_tags_info: குறிச்சொற்களின் அடிப்படையில் இந்த பத்திரிகையில்
ஃபெடிவர்சிலிருந்து உள்ளடக்கம் சேர்க்கப்பட வேண்டும் என்று நீங்கள் விரும்பினால்
மட்டுமே வழங்கவும்
auto_preview: ஆட்டோ மீடியா முன்னோட்டம்
dynamic_lists: மாறும் பட்டியல்கள்
banned_instances: தடைசெய்யப்பட்ட நிகழ்வுகள்
kbin_intro_title: ஃபெடிவர்சை ஆராயுங்கள்
kbin_intro_desc: ஃபெடிவர்ச் நெட்வொர்க்கில் செயல்படும் உள்ளடக்க திரட்டல் மற்றும்
மைக்ரோ பிளாக்கிங் செய்வதற்கான ஒரு பரவலாக்கப்பட்ட தளமாகும்.
kbin_promo_title: உங்கள் சொந்த நிகழ்வை உருவாக்கவும்
kbin_promo_desc: '%link_start%குளோன் ரெப்போ%இணைப்பு_எண்ட்%மற்றும் ஃபெடிவர்சை உருவாக்குங்கள்'
captcha_enabled: கேப்ட்சா இயக்கப்பட்டது
header_logo: தலைப்பு லோகோ
browsing_one_thread: விவாதத்தில் நீங்கள் ஒரு நூலை மட்டுமே உலாவுகிறீர்கள்!
அனைத்து கருத்துகளும் தபால் பக்கத்தில் கிடைக்கின்றன.
return: திரும்ப
boost: பூச்ட்
mercure_enabled: மெர்குர் இயக்கப்பட்டது
report_issue: சிக்கல் அறிக்கை
tokyo_night: டோக்கியோ இரவு
preferred_languages: நூல்கள் மற்றும் இடுகைகளின் மொழிகளை வடிகட்டவும்
infinite_scroll_help: நீங்கள் பக்கத்தின் அடிப்பகுதியை அடையும்போது தானாக அதிக
உள்ளடக்கத்தை ஏற்றவும்.
auto_preview_help: உள்ளடக்கத்திற்கு கீழே பெரிய அளவில் மீடியா (புகைப்படம்,
வீடியோ) முன்னோட்டங்களைக் காட்டுங்கள்.
reload_to_apply: மாற்றங்களைப் பயன்படுத்த பக்கத்தை மீண்டும் ஏற்றவும்
filter.origin.label: தோற்றத்தைத் தேர்வுசெய்க
filter.fields.label: எந்த புலங்களைத் தேட வேண்டும் என்பதைத் தேர்வுசெய்க
sticky_navbar_help: நீங்கள் கீழே உருட்டும்போது நவ்பார் பக்கத்தின் மேற்புறத்தில்
ஒட்டிக்கொண்டிருக்கும்.
filter.adult.label: NSFW ஐக் காண்பிக்க வேண்டுமா என்பதைத் தேர்வுசெய்க
filter.adult.hide: NSFW ஐ மறைக்கவும்
filter.adult.show: NSFW ஐக் காட்டு
filter.adult.only: NSFW மட்டுமே
local_and_federated: உள்ளக மற்றும் கூட்டாட்சி
filter.fields.only_names: பெயர்கள் மட்டுமே
filter.fields.names_and_descriptions: பெயர்கள் மற்றும் விளக்கங்கள்
kbin_bot: Ing முகவர்
bot_body_content: "Mbin முகவருக்கு வருக! MBIN க்குள் செயல்பாட்டு பப் செயல்பாட்டை செயல்படுத்துவதில்
இந்த முகவர் முக்கிய பங்கு வகிக்கிறது. ஃபெடிவர்சில் உள்ள பிற நிகழ்வுகளுடன் MBIN தொடர்பு
கொள்ளவும் கூட்டமாகவும் இருக்க முடியும் என்பதை இது உறுதி செய்கிறது.\n\n செயல்பாட்டு
பப் என்பது ஒரு திறந்த நிலையான நெறிமுறையாகும், இது பரவலாக்கப்பட்ட சமூக வலைப்பின்னல்
தளங்களை ஒருவருக்கொருவர் தொடர்பு கொள்ளவும் தொடர்பு கொள்ளவும் அனுமதிக்கிறது. ஃபெடிவர்ச்
என அழைக்கப்படும் கூட்டாட்சி சமூக வலைப்பின்னல் முழுவதும் உள்ளடக்கத்தைப் பின்பற்றவும்,
தொடர்பு கொள்ளவும், பகிர்ந்து கொள்ளவும் வெவ்வேறு நிகழ்வுகளில் (சேவையகங்கள்) பயனர்களுக்கு
இது உதவுகிறது. பயனர்கள் உள்ளடக்கத்தை வெளியிடுவதற்கும், பிற பயனர்களைப் பின்தொடர்வதற்கும்,
நூல்கள் அல்லது இடுகைகளில் விரும்புவது, பகிர்வது மற்றும் கருத்து தெரிவிப்பது போன்ற
சமூக தொடர்புகளில் ஈடுபடுவதற்கும் இது ஒரு தரப்படுத்தப்பட்ட வழியை வழங்குகிறது."
password_confirm_header: உங்கள் கடவுச்சொல் மாற்ற கோரிக்கையை உறுதிப்படுத்தவும்.
your_account_is_not_active: உங்கள் கணக்கு செயல்படுத்தப்படவில்லை. கணக்கு
செயல்படுத்தும் வழிமுறைகளுக்கு உங்கள் மின்னஞ்சலைச் சரிபார்க்கவும் அல்லது புதிய கணக்கு செயல்படுத்தும் மின்னஞ்சலைக் கோருங்கள்.
your_account_has_been_banned: உங்கள் கணக்கு தடைசெய்யப்பட்டுள்ளது
your_account_is_not_yet_approved: உங்கள் கணக்கு இன்னும் அங்கீகரிக்கப்படவில்லை.
உங்கள் பதிவுபெறும் கோரிக்கையை நிர்வாகிகள் செயலாக்கியவுடன் நாங்கள் உங்களுக்கு
ஒரு மின்னஞ்சல் அனுப்புவோம்.
toolbar.bold: தடிமான
toolbar.italic: சாய்வு
toolbar.strikethrough: ச்ட்ரைகெத்ரோ
toolbar.header: தலைப்பி
toolbar.quote: மேற்கோள்
toolbar.code: குறியீடு
toolbar.link: இணைப்பு
toolbar.image: படம்
toolbar.unordered_list: வரிசைப்படுத்தப்படாத பட்டியல்
toolbar.ordered_list: ஆர்டர் செய்யப்பட்ட பட்டியல்
toolbar.mention: குறிப்பு
federation_page_allowed_description: அறியப்பட்ட நிகழ்வுகள் நாங்கள் கூட்டுறவு
கொள்கிறோம்
toolbar.spoiler: இறக்கைத்தடை
federation_page_enabled: கூட்டமைப்பு பக்கம் இயக்கப்பட்டது
federation_page_disallowed_description: நாங்கள் கூட்டுறவு கொள்ளாத நிகழ்வுகள்
federation_page_dead_title: இறந்த நிகழ்வுகள்
federated_search_only_loggedin: உள்நுழையவில்லை என்றால் ஃபெடரேட்டட் தேடல்
லிமிடெட்
account_deletion_title: கணக்கு நீக்குதல்
federation_page_dead_description: ஒரு வரிசையில் குறைந்தது 10 நடவடிக்கைகளை
எங்களால் வழங்க முடியவில்லை, கடைசியாக வெற்றிகரமான மகப்பேறு மற்றும் ரெசிவ் ஒரு
வாரத்திற்கு முன்னர் இருந்தன
account_deletion_description: கணக்கை உடனடியாக நீக்க நீங்கள் தேர்வு
செய்யாவிட்டால் உங்கள் கணக்கு 30 நாட்களில் நீக்கப்படும். 30 நாட்களுக்குள்
உங்கள் கணக்கை மீட்டெடுக்க, ஒரே பயனர் சான்றுகளுடன் உள்நுழைக அல்லது நிர்வாகியை
தொடர்பு கொள்ளவும்.
account_deletion_immediate: உடனடியாக நீக்கு
more_from_domain: டொமைனில் இருந்து மேலும்
errors.server500.description: மன்னிக்கவும், எங்கள் முடிவில் ஏதோ தவறு ஏற்பட்டது.
இந்த பிழையை நீங்கள் தொடர்ந்து பார்த்தால், நிகழ்வு உரிமையாளரைத் தொடர்பு கொள்ள
முயற்சிக்கவும். இந்த நிகழ்வு செயல்படவில்லை என்றால், இதற்கிடையில் சிக்கல்
தீர்க்கப்படும் வரை%link_start%பிற MBIN நிகழ்வுகள்%இணைப்பு_என்ட்%ஐப் பாருங்கள்.
errors.server429.title: 429 பல கோரிக்கைகள்
errors.server404.title: 404 கண்டுபிடிக்கப்படவில்லை
errors.server403.title: 403 தடைசெய்யப்பட்டுள்ளது
email.delete.title: பயனர் கணக்கு நீக்குதல் கோரிக்கை
email_confirm_button_text: உங்கள் கடவுச்சொல் மாற்ற கோரிக்கையை உறுதிப்படுத்தவும்
email_confirm_link_help: மாற்றாக நீங்கள் பின்வருவனவற்றை உங்கள் உலாவியில்
நகலெடுத்து ஒட்டலாம்
email.delete.description: பின்வரும் பயனர் தங்கள் கணக்கை நீக்குமாறு கோரியுள்ளார்
resend_account_activation_email_question: செயலற்ற கணக்கு?
resend_account_activation_email: கணக்கு செயல்படுத்தும் மின்னஞ்சலை மீண்டும்
வழங்கவும்
resend_account_activation_email_error: இந்த கோரிக்கையை சமர்ப்பிப்பதில் சிக்கல்
இருந்தது. அந்த மின்னஞ்சலுடன் தொடர்புடைய எந்தக் கணக்கும் இருக்கலாம் அல்லது அது
ஏற்கனவே செயல்படுத்தப்பட்டிருக்கலாம்.
resend_account_activation_email_success: அந்த மின்னஞ்சலுடன் தொடர்புடைய ஒரு
கணக்கு இருந்தால், நாங்கள் ஒரு புதிய செயல்படுத்தல் மின்னஞ்சலை அனுப்புவோம்.
resend_account_activation_email_description: உங்கள் கணக்குடன் தொடர்புடைய
மின்னஞ்சல் முகவரியை உள்ளிடவும். உங்களுக்காக மற்றொரு செயல்படுத்தும் மின்னஞ்சலை
நாங்கள் அனுப்புவோம்.
custom_css: தனிப்பயன் சிஎச்எச்
oauth.consent.title: OAuth2 ஒப்புதல் படிவம்
oauth.consent.grant_permissions: அனுமதிகள் வழங்கவும்
ignore_magazines_custom_css: பத்திரிகைகள் தனிப்பயன் சிஎச்எச் ஐ புறக்கணிக்கவும்
oauth.consent.app_requesting_permissions: உங்கள் சார்பாக பின்வரும் செயல்களைச்
செய்ய விரும்புகிறேன்
oauth.consent.app_has_permissions: ஏற்கனவே பின்வரும் செயல்களைச் செய்யலாம்
oauth.consent.to_allow_access: இந்த அணுகலை அனுமதிக்க, கீழே உள்ள 'இசைவு'
பொத்தானைக் சொடுக்கு செய்க
oauth.consent.allow: இசைவு
oauth.consent.deny: மறுக்கவும்
oauth.client_identifier.invalid: தவறான OAUTH கிளையன்ட் ஐடி!
oauth.client_not_granted_message_read_permission: உங்கள் செய்திகளைப் படிக்க இந்த
பயன்பாட்டிற்கு இசைவு கிடைக்கவில்லை.
restrict_oauth_clients: OAuth2 கிளையன்ட் உருவாக்கத்தை நிர்வாகிகளுக்கு
கட்டுப்படுத்துங்கள்
private_instance: எந்தவொரு உள்ளடக்கத்தையும் அணுகுவதற்கு முன்பு பயனர்களை உள்நுழைய
கட்டாயப்படுத்துங்கள்
block: தொகுதி
unblock: தடை
oauth2.grant.moderate.magazine.ban.delete: உங்கள் மிதமான பத்திரிகைகளில் தடையற்ற
பயனர்கள்.
oauth2.grant.moderate.magazine.list: உங்கள் மிதமான பத்திரிகைகளின் பட்டியலைப்
படியுங்கள்.
oauth2.grant.moderate.magazine.reports.all: உங்கள் மிதமான பத்திரிகைகளில்
அறிக்கைகளை நிர்வகிக்கவும்.
oauth2.grant.moderate.magazine.reports.read: உங்கள் மிதமான பத்திரிகைகளில்
அறிக்கைகளைப் படியுங்கள்.
oauth2.grant.moderate.magazine.reports.action: உங்கள் மிதமான பத்திரிகைகளில்
அறிக்கைகளை ஏற்கவும் அல்லது நிராகரிக்கவும்.
oauth2.grant.moderate.magazine.trash.read: உங்கள் மிதமான பத்திரிகைகளில்
குப்பைத்தொட்டிய உள்ளடக்கத்தைக் காண்க.
oauth2.grant.admin.entry.purge: உங்கள் நிகழ்விலிருந்து எந்த நூலையும் முழுமையாக
நீக்கவும்.
oauth2.grant.read.general: நீங்கள் அணுகக்கூடிய அனைத்து உள்ளடக்கங்களையும்
படியுங்கள்.
oauth2.grant.write.general: உங்கள் நூல்கள், இடுகைகள் அல்லது கருத்துகள் ஏதேனும்
ஒன்றை உருவாக்கவும் அல்லது திருத்தவும்.
oauth2.grant.delete.general: உங்கள் நூல்கள், இடுகைகள் அல்லது கருத்துகள் ஏதேனும்
ஒன்றை நீக்கவும்.
oauth2.grant.moderate.magazine_admin.all: உங்களுக்கு சொந்தமான பத்திரிகைகளை
உருவாக்கவும், திருத்தவும் அல்லது நீக்கவும்.
oauth2.grant.moderate.magazine_admin.create: புதிய பத்திரிகைகளை உருவாக்கவும்.
oauth2.grant.moderate.magazine_admin.delete: உங்களுக்கு சொந்தமான எதையும் நீக்கு.
oauth2.grant.moderate.magazine_admin.update: உங்களுக்கு சொந்தமான எந்த
பத்திரிகைகளின் விதிகள், விளக்கம், NSFW நிலை அல்லது ஐகானைத் திருத்தவும்.
oauth2.grant.moderate.magazine_admin.edit_theme: உங்களுக்கு சொந்தமான எந்தவொரு
பத்திரிகைகளின் தனிப்பயன் சிஎச்எச் ஐத் திருத்தவும்.
oauth2.grant.moderate.magazine_admin.moderators: உங்களுக்கு சொந்தமான எந்தவொரு
பத்திரிகைகளின் மதிப்பீட்டாளர்களையும் சேர்க்கவும் அல்லது அகற்றவும்.
oauth2.grant.moderate.magazine_admin.badges: உங்களுக்கு சொந்தமான
பத்திரிகைகளிலிருந்து பேட்ச்களை உருவாக்கவும் அல்லது அகற்றவும்.
oauth2.grant.moderate.magazine_admin.tags: உங்களுக்கு சொந்தமான
பத்திரிகைகளிலிருந்து குறிச்சொற்களை உருவாக்கவும் அல்லது அகற்றவும்.
oauth2.grant.moderate.magazine_admin.stats: உங்களுக்கு சொந்தமான பத்திரிகைகளின்
உள்ளடக்கம், வாக்களிப்பு மற்றும் புள்ளிவிவரங்களைக் காண்க.
oauth2.grant.admin.all: உங்கள் நிகழ்வில் எந்தவொரு நிர்வாக நடவடிக்கையும்
செய்யுங்கள்.
oauth2.grant.report.general: நூல்கள், இடுகைகள் அல்லது கருத்துகளைப்
புகாரளிக்கவும்.
oauth2.grant.subscribe.general: எந்தவொரு செய்தித் தாள், டொமைன் அல்லது பயனரையும்
குழுசேரவும் அல்லது பின்பற்றவும், நீங்கள் குழுசேரும் பத்திரிகைகள், களங்கள்
மற்றும் பயனர்களைக் காண்க.
oauth2.grant.block.general: எந்தவொரு செய்தித் தாள், டொமைன் அல்லது பயனரைத்
தடுத்து அல்லது தடைசெய்க, நீங்கள் தடுத்த பத்திரிகைகள், களங்கள் மற்றும்
பயனர்களைக் காண்க.
oauth2.grant.vote.general: நூல்கள், இடுகைகள் அல்லது கருத்துகளை உயர்த்தவும்,
குறைத்து மதிப்பிடவும் அல்லது உயர்த்தவும்.
oauth2.grant.domain.all: களங்களுக்கு குழுசேரவும் அல்லது தடுக்கவும், நீங்கள்
குழுசேரும் களங்களை அல்லது தடுக்கவும்.
oauth2.grant.domain.subscribe: களங்களுக்கு குழுசேரவும் அல்லது குழுவிலகவும்
மற்றும் நீங்கள் குழுசேரும் களங்களைக் காண்க.
oauth2.grant.domain.block: களங்களைத் தடுத்து அல்லது தடைசெய்தல் மற்றும் நீங்கள்
தடுத்த களங்களைக் காண்க.
oauth2.grant.entry.all: உங்கள் நூல்களை உருவாக்கவும், திருத்தவும் அல்லது
நீக்கவும், எந்த நூலையும் வாக்களிக்கவும், உயர்த்தவும் அல்லது புகாரளிக்கவும்.
oauth2.grant.entry.create: புதிய நூல்களை உருவாக்கவும்.
oauth2.grant.post.delete: உங்கள் இருக்கும் இடுகைகளை நீக்கவும்.
oauth2.grant.post.vote: எந்தவொரு இடுகையையும் உயர்த்தவும், பூச்ட் செய்யவும்
அல்லது குறைக்கவும்.
oauth2.grant.post.report: எந்த இடுகையையும் புகாரளிக்கவும்.
oauth2.grant.entry.edit: உங்கள் இருக்கும் நூல்களைத் திருத்தவும்.
oauth2.grant.entry.delete: உங்கள் இருக்கும் நூல்களை நீக்கவும்.
oauth2.grant.entry.vote: எந்தவொரு நூலையும் உயர்த்தவும், உயர்த்தவும் அல்லது
குறைக்கவும்.
oauth2.grant.entry.report: எந்த நூலையும் புகாரளிக்கவும்.
oauth2.grant.entry_comment.all: உங்கள் கருத்துகளை நூல்களில் உருவாக்கவும்,
திருத்தவும் அல்லது நீக்கவும், வாக்களிக்கவும், உயர்த்தவும் அல்லது எந்தவொரு
கருத்தையும் ஒரு நூலில் புகாரளிக்கவும்.
oauth2.grant.entry_comment.create: நூல்களில் புதிய கருத்துகளை உருவாக்கவும்.
oauth2.grant.entry_comment.edit: உங்கள் இருக்கும் கருத்துகளை நூல்களில்
திருத்தவும்.
oauth2.grant.entry_comment.delete: உங்கள் இருக்கும் கருத்துகளை நூல்களில்
நீக்கவும்.
oauth2.grant.entry_comment.vote: எந்தவொரு கருத்தையும் ஒரு நூலில் உயர்த்தவும்,
உயர்த்தவும் அல்லது குறைக்கவும்.
oauth2.grant.entry_comment.report: எந்தவொரு கருத்தையும் ஒரு நூலில்
புகாரளிக்கவும்.
oauth2.grant.magazine.all: பத்திரிகைகளுக்கு குழுசேரவும் அல்லது தடுக்கவும்,
நீங்கள் குழுசேரும் அல்லது தடுக்கும் பத்திரிகைகளைக் காண்க.
oauth2.grant.magazine.subscribe: பத்திரிகைகளுக்கு குழுசேரவும் அல்லது
குழுவிலகவும் மற்றும் நீங்கள் குழுசேரும் பத்திரிகைகளைப் பார்க்கவும்.
oauth2.grant.magazine.block: பத்திரிகைகளைத் தடுத்து நிறுத்தி, நீங்கள் தடுத்த
பத்திரிகைகளைப் பார்க்கவும்.
oauth2.grant.post.all: உங்கள் மைக்ரோ வலைப்பதிவுகளை உருவாக்கவும், திருத்தவும்
அல்லது நீக்கவும், வாக்களிக்கவும், பூச்ட் செய்யவும் அல்லது எந்த மைக்ரோ
வலைப்பதிவைப் புகாரளிக்கவும்.
oauth2.grant.post.create: புதிய இடுகைகளை உருவாக்கவும்.
oauth2.grant.post.edit: உங்கள் இருக்கும் இடுகைகளைத் திருத்தவும்.
oauth2.grant.post_comment.all: இடுகைகளில் உங்கள் கருத்துகளை உருவாக்கவும்,
திருத்தவும் அல்லது நீக்கவும், ஒரு இடுகையில் வாக்களிக்கவும், உயர்த்தவும் அல்லது
எந்த கருத்தையும் புகாரளிக்கவும்.
oauth2.grant.post_comment.create: இடுகைகளில் புதிய கருத்துகளை உருவாக்கவும்.
oauth2.grant.post_comment.edit: இடுகைகளில் உங்கள் இருக்கும் கருத்துகளைத்
திருத்தவும்.
oauth2.grant.post_comment.delete: இடுகைகளில் உங்கள் இருக்கும் கருத்துகளை
நீக்கவும்.
oauth2.grant.post_comment.vote: ஒரு இடுகையில் எந்தவொரு கருத்தையும் உயர்த்தவும்,
பூச்ட் செய்யவும் அல்லது குறைத்து மதிப்பிடவும்.
oauth2.grant.post_comment.report: ஒரு இடுகையில் எந்த கருத்தையும் தெரிவிக்கவும்.
oauth2.grant.user.all: உங்கள் சுயவிவரம், செய்திகள் அல்லது அறிவிப்புகளைப் படித்து
திருத்தவும்; நீங்கள் பிற பயன்பாடுகளை வழங்கிய அனுமதிகளைப் படித்து திருத்தவும்;
பிற பயனர்களைப் பின்தொடரவும் அல்லது தடுக்கவும்; நீங்கள் பின்பற்றும் அல்லது
தடுக்கும் பயனர்களின் பட்டியல்களைக் காண்க.
oauth2.grant.user.profile.read: உங்கள் சுயவிவரத்தைப் படியுங்கள்.
oauth2.grant.user.profile.edit: உங்கள் சுயவிவரத்தைத் திருத்தவும்.
oauth2.grant.user.profile.all: உங்கள் சுயவிவரத்தைப் படித்து திருத்தவும்.
oauth2.grant.user.message.all: உங்கள் செய்திகளைப் படித்து பிற பயனர்களுக்கு
செய்திகளை அனுப்பவும்.
oauth2.grant.user.message.read: உங்கள் செய்திகளைப் படியுங்கள்.
oauth2.grant.user.message.create: பிற பயனர்களுக்கு செய்திகளை அனுப்பவும்.
oauth2.grant.user.notification.all: உங்கள் அறிவிப்புகளைப் படித்து அழிக்கவும்.
oauth2.grant.user.notification.read: செய்தி அறிவிப்புகள் உட்பட உங்கள்
அறிவிப்புகளைப் படியுங்கள்.
oauth2.grant.user.notification.delete: உங்கள் அறிவிப்புகளை அழிக்கவும்.
oauth2.grant.user.oauth_clients.all: பிற OAuth2 விண்ணப்பங்களுக்கு நீங்கள்
வழங்கிய அனுமதிகளைப் படித்து திருத்தவும்.
oauth2.grant.user.oauth_clients.read: பிற OAuth2 விண்ணப்பங்களுக்கு நீங்கள்
வழங்கிய அனுமதிகளைப் படியுங்கள்.
oauth2.grant.user.oauth_clients.edit: பிற OAuth2 விண்ணப்பங்களுக்கு நீங்கள்
வழங்கிய அனுமதிகளைத் திருத்தவும்.
oauth2.grant.user.follow: பயனர்களைப் பின்தொடரவும் அல்லது பின்தொடரவும், நீங்கள்
பின்பற்றும் பயனர்களின் பட்டியலைப் படியுங்கள்.
oauth2.grant.user.block: பயனர்களைத் தடு அல்லது தடைசெய்க, நீங்கள் தடுக்கும்
பயனர்களின் பட்டியலைப் படியுங்கள்.
oauth2.grant.moderate.all: உங்கள் மிதமான பத்திரிகைகளில் செய்ய உங்களுக்கு இசைவு
உள்ள எந்த மிதமான செயலையும் செய்யுங்கள்.
oauth2.grant.moderate.entry.all: உங்கள் மிதமான பத்திரிகைகளில் மிதமான நூல்கள்.
oauth2.grant.moderate.entry.change_language: உங்கள் மிதமான பத்திரிகைகளில்
நூல்களின் மொழியை மாற்றவும்.
oauth2.grant.moderate.entry.pin: உங்கள் மிதமான பத்திரிகைகளின் மேற்புறத்தில் முள்
நூல்கள்.
oauth2.grant.moderate.entry.set_adult: உங்கள் மிதமான பத்திரிகைகளில் நூல்களை NSFW
ஆக குறிக்கவும்.
oauth2.grant.moderate.entry.trash: உங்கள் மிதமான பத்திரிகைகளில் நூல்களை குப்பை
அல்லது மீட்டமை.
oauth2.grant.moderate.entry_comment.change_language: உங்கள் மிதமான
பத்திரிகைகளில் உள்ள நூல்களில் கருத்துகளின் மொழியை மாற்றவும்.
oauth2.grant.moderate.entry_comment.all: உங்கள் மிதமான பத்திரிகைகளில் நூல்களில்
மிதமான கருத்துகள்.
oauth2.grant.moderate.entry_comment.set_adult: உங்கள் மிதமான பத்திரிகைகளில் NSFW
ஆக நூல்களில் கருத்துகளை குறிக்கவும்.
oauth2.grant.moderate.entry_comment.trash: உங்கள் மிதமான பத்திரிகைகளில்
நூல்களில் கருத்துகளை குப்பை அல்லது மீட்டமைக்கவும்.
oauth2.grant.moderate.post.all: உங்கள் மிதமான பத்திரிகைகளில் மிதமான இடுகைகள்.
oauth2.grant.moderate.post.change_language: உங்கள் மிதமான பத்திரிகைகளில்
இடுகைகளின் மொழியை மாற்றவும்.
oauth2.grant.moderate.post.set_adult: உங்கள் மிதமான பத்திரிகைகளில் இடுகைகளை NSFW
ஆக குறிக்கவும்.
oauth2.grant.moderate.post.trash: உங்கள் மிதமான பத்திரிகைகளில் இடுகைகளை குப்பை
அல்லது மீட்டமை.
oauth2.grant.moderate.post_comment.all: உங்கள் மிதமான பத்திரிகைகளில் இடுகைகளில்
மிதமான கருத்துகள்.
oauth2.grant.moderate.post_comment.change_language: உங்கள் மிதமான பத்திரிகைகளில்
இடுகைகளில் கருத்துகளின் மொழியை மாற்றவும்.
oauth2.grant.moderate.post_comment.set_adult: உங்கள் மிதமான பத்திரிகைகளில்
இடுகைகளில் கருத்துகளை NSFW ஆகக் குறிக்கவும்.
oauth2.grant.moderate.post_comment.trash: உங்கள் மிதமான பத்திரிகைகளில்
இடுகைகளில் கருத்துகளை குப்பை அல்லது மீட்டெடுக்கவும்.
oauth2.grant.moderate.magazine.ban.all: உங்கள் மிதமான பத்திரிகைகளில்
தடைசெய்யப்பட்ட பயனர்களை நிர்வகிக்கவும்.
oauth2.grant.moderate.magazine.ban.read: உங்கள் மிதமான பத்திரிகைகளில்
தடைசெய்யப்பட்ட பயனர்களைக் காண்க.
oauth2.grant.moderate.magazine.all: உங்கள் மிதமான பத்திரிகைகளில்
குப்பைத்தொட்டியான பொருட்களைக் காண்க, அறிக்கைகள் மற்றும் பார்வை.
oauth2.grant.moderate.magazine.ban.create: உங்கள் மிதமான பத்திரிகைகளில் பயனர்களை
தடை செய்யுங்கள்.
oauth2.grant.admin.entry_comment.purge: உங்கள் நிகழ்விலிருந்து ஒரு நூலில் உள்ள
எந்த கருத்தையும் முழுமையாக நீக்கவும்.
oauth2.grant.admin.post.purge: உங்கள் நிகழ்விலிருந்து எந்த இடுகையையும் முழுமையாக
நீக்கவும்.
oauth2.grant.admin.magazine.move_entry: உங்கள் நிகழ்வில் பத்திரிகைகளுக்கு
இடையில் நூல்களை நகர்த்தவும்.
oauth2.grant.admin.magazine.purge: உங்கள் நிகழ்வில் பத்திரிகைகளை முழுவதுமாக
நீக்கவும்.
oauth2.grant.admin.post_comment.purge: உங்கள் நிகழ்விலிருந்து ஒரு இடுகையின்
எந்தவொரு கருத்தையும் முழுமையாக நீக்கவும்.
oauth2.grant.admin.magazine.all: உங்கள் நிகழ்வில் பத்திரிகைகளுக்கு இடையில்
நூல்களை நகர்த்தவும் அல்லது முழுமையாக நீக்கவும்.
oauth2.grant.admin.user.all: உங்கள் நிகழ்வில் பயனர்களை தடை செய்யுங்கள்,
சரிபார்க்கவும் அல்லது முழுமையாக நீக்கவும்.
oauth2.grant.admin.user.ban: உங்கள் நிகழ்விலிருந்து தடை அல்லது தடைசெய்யும்
பயனர்கள்.
oauth2.grant.admin.user.verify: உங்கள் நிகழ்வில் பயனர்களை சரிபார்க்கவும்.
oauth2.grant.admin.user.delete: உங்கள் நிகழ்விலிருந்து பயனர்களை நீக்கு.
oauth2.grant.admin.user.purge: உங்கள் நிகழ்விலிருந்து பயனர்களை முழுமையாக
நீக்கவும்.
oauth2.grant.admin.instance.all: நிகழ்வு அமைப்புகள் அல்லது தகவல்களைக் காணலாம்
மற்றும் புதுப்பிக்கவும்.
oauth2.grant.admin.instance.stats: உங்கள் நிகழ்வின் புள்ளிவிவரங்களைக் காண்க.
oauth2.grant.admin.instance.settings.edit: உங்கள் நிகழ்வில் அமைப்புகளைப்
புதுப்பிக்கவும்.
oauth2.grant.admin.instance.settings.all: உங்கள் நிகழ்வில் அமைப்புகளைப்
பார்க்கவும் அல்லது புதுப்பிக்கவும்.
oauth2.grant.admin.instance.settings.read: உங்கள் நிகழ்வில் அமைப்புகளைக் காண்க.
oauth2.grant.admin.instance.information.edit: உங்கள் நிகழ்வில் கேள்விகள்,
தொடர்பு, பணி விதிமுறைகள் மற்றும் தனியுரிமைக் கொள்கை பக்கங்களைப்
புதுப்பிக்கவும்.
oauth2.grant.admin.federation.all: தற்போது வரையறுக்கப்பட்ட நிகழ்வுகளைக் காணவும்
புதுப்பிக்கவும்.
oauth2.grant.admin.federation.read: வரையறுக்கப்பட்ட நிகழ்வுகளின் பட்டியலைக்
காண்க.
oauth2.grant.admin.oauth_clients.all: உங்கள் நிகழ்வில் இருக்கும் OAuth2
வாடிக்கையாளர்களைக் காண்க அல்லது ரத்து செய்யவும்.
oauth2.grant.admin.oauth_clients.read: உங்கள் நிகழ்வில் இருக்கும் OAuth2
வாடிக்கையாளர்களையும் அவற்றின் பயன்பாட்டு புள்ளிவிவரங்களையும் காண்க.
oauth2.grant.admin.federation.update: வரையறுக்கப்பட்ட நிகழ்வுகளின்
பட்டியலிலிருந்து அல்லது நிகழ்வுகளைச் சேர்க்கவும் அல்லது அகற்றவும்.
oauth2.grant.admin.oauth_clients.revoke: உங்கள் நிகழ்வில் OAuth2
வாடிக்கையாளர்களுக்கான அணுகலைத் திரும்பப் பெறுங்கள்.
last_active: கடைசியாக செயலில்
flash_post_pin_success: இடுகை வெற்றிகரமாக பொருத்தப்பட்டுள்ளது.
flash_post_unpin_success: இந்த இடுகை வெற்றிகரமாக இணைக்கப்படவில்லை.
comment_reply_position_help: கருத்து பதில் படிவத்தை பக்கத்தின் மேல் அல்லது கீழ்
காண்பிக்கவும். 'எல்லையற்ற சுருள்' இயக்கப்பட்டால், நிலை எப்போதும் மேலே
தோன்றும்.
show_avatars_on_comments: கருத்து அவதாரங்களைக் காட்டு
single_settings: ஒற்றை
update_comment: கருத்தைப் புதுப்பிக்கவும்
show_avatars_on_comments_help: ஒற்றை நூல் அல்லது இடுகையில் கருத்துகளைப்
பார்க்கும்போது பயனர் அவதாரங்களைக் காண்பி/மறைக்கவும்.
comment_reply_position: கருத்து பதில் நிலை
magazine_theme_appearance_custom_css: உங்கள் பத்திரிகைக்குள் உள்ளடக்கத்தைப்
பார்க்கும்போது பொருந்தும் தனிப்பயன் CSS.
magazine_theme_appearance_icon: பத்திரிகைக்கான தனிப்பயன் படவுரு. எதுவும்
தேர்ந்தெடுக்கப்படவில்லை என்றால், இயல்புநிலை படவுரு பயன்படுத்தப்படும்.
magazine_theme_appearance_background_image: உங்கள் பத்திரிகைக்குள்
உள்ளடக்கத்தைப் பார்க்கும்போது பயன்படுத்தப்படும் தனிப்பயன் பின்னணி படம்.
moderation.report.approve_report_title: ஒப்புதல் அறிக்கை
moderation.report.reject_report_title: அறிக்கையை நிராகரிக்கவும்
moderation.report.ban_user_description: இந்த பத்திரிகையிலிருந்து இந்த
உள்ளடக்கத்தை உருவாக்கிய பயனரை (%பயனர்பெயர்%) தடை செய்ய விரும்புகிறீர்களா?
moderation.report.approve_report_confirmation: இந்த அறிக்கையை நீங்கள்
அங்கீகரிக்க விரும்புகிறீர்கள் என்பதில் உறுதியாக இருக்கிறீர்களா?
subject_reported_exists: இந்த உள்ளடக்கம் ஏற்கனவே அறிவிக்கப்பட்டுள்ளது.
moderation.report.ban_user_title: பயனரை தடை செய்யுங்கள்
moderation.report.reject_report_confirmation: இந்த அறிக்கையை நிராகரிக்க
விரும்புகிறீர்கள் என்பதில் உறுதியாக இருக்கிறீர்களா?
oauth2.grant.moderate.post.pin: உங்கள் மிதமான பத்திரிகைகளின் மேலே இடுகைகளை முள்.
purge_content: உள்ளடக்கத்தை தூய்மைப்படுத்துங்கள்
delete_content_desc: உருவாக்கப்பட்ட நூல்கள், இடுகைகள் மற்றும் கருத்துகளில் பிற
பயனர்களின் பதில்களை விட்டு வெளியேறும்போது பயனரின் உள்ளடக்கத்தை நீக்கவும்.
delete_account_desc: உருவாக்கப்பட்ட நூல்கள், இடுகைகள் மற்றும் கருத்துகளில் பிற
பயனர்களின் பதில்கள் உட்பட கணக்கை நீக்கவும்.
schedule_delete_account: அட்டவணை நீக்குதல்
purge_content_desc: உருவாக்கப்பட்ட நூல்கள், இடுகைகள் மற்றும் கருத்துகளில் பிற
பயனர்களின் பதில்களை நீக்குவது உட்பட பயனரின் உள்ளடக்கத்தை முற்றிலுமாக
தூய்மைப்படுத்துங்கள்.
schedule_delete_account_desc: இந்த கணக்கை நீக்குவதற்கு 30 நாட்களில்
திட்டமிடவும். இது பயனரையும் அவற்றின் உள்ளடக்கத்தையும் மறைக்கும், அத்துடன்
பயனரை உள்நுழைவதைத் தடுக்கும்.
remove_schedule_delete_account: திட்டமிடப்பட்ட நீக்குதலை அகற்று
remove_schedule_delete_account_desc: திட்டமிடப்பட்ட நீக்குதலை அகற்றவும். எல்லா
உள்ளடக்கங்களும் மீண்டும் கிடைக்கும், மேலும் பயனருக்கு உள்நுழைய முடியும்.
two_factor_backup: இரண்டு காரணி அங்கீகார காப்புப்பிரதி குறியீடுகள்
2fa.authentication_code.label: அங்கீகார குறியீடு
2fa.verify: சரிபார்க்கவும்
2fa.code_invalid: அங்கீகார குறியீடு செல்லுபடியாகாது
2fa.disable: இரண்டு காரணி அங்கீகாரத்தை முடக்கு
2fa.backup: உங்கள் இரண்டு காரணி காப்பு குறியீடுகள்
2fa.backup-create.help: நீங்கள் புதிய காப்பு அங்கீகார குறியீடுகளை உருவாக்கலாம்;
அவ்வாறு செய்வது ஏற்கனவே உள்ள குறியீடுகளை செல்லாது.
2fa.backup-create.label: புதிய காப்பு அங்கீகார குறியீடுகளை உருவாக்கவும்
2fa.remove: 2fa ஐ அகற்று
2fa.add: எனது கணக்கில் சேர்க்கவும்
2fa.verify_authentication_code.label: அமைப்பை சரிபார்க்க இரண்டு காரணி குறியீட்டை
உள்ளிடவும்
2fa.qr_code_img.alt: உங்கள் கணக்கிற்கான இரண்டு காரணி அங்கீகாரத்தை அமைக்க
அனுமதிக்கும் QR குறியீடு
2fa.user_active_tfa.title: பயனருக்கு செயலில் 2FA உள்ளது
2fa.qr_code_link.title: இந்த இணைப்பைப் பார்வையிடுவது இந்த இரண்டு காரணி
அங்கீகாரத்தை பதிவு செய்ய உங்கள் தளத்தை அனுமதிக்கலாம்
2fa.available_apps: QR-குறியீட்டை ச்கேன் செய்ய %google_authenticator %, %aegis
%(Android) அல்லது %raivo %(iOS) போன்ற இரண்டு காரணி அங்கீகார பயன்பாட்டைப்
பயன்படுத்தவும்.
2fa.backup_codes.recommendation: அவற்றின் நகலை பாதுகாப்பான இடத்தில் வைத்திருக்க
பரிந்துரைக்கப்படுகிறது.
2fa.backup_codes.help: உங்கள் இரண்டு காரணி அங்கீகார சாதனம் அல்லது பயன்பாடு
இல்லாதபோது இந்த குறியீடுகளைப் பயன்படுத்தலாம். நீங்கள் அவற்றைக் காட்ட
மாட்டீர்கள் மற்றும் அவை ஒவ்வொன்றையும் ஒரு முறை மட்டும்
ஐப் பயன்படுத்த முடியும்.
password_and_2fa: கடவுச்சொல் மற்றும் 2FA
flash_account_settings_changed: உங்கள் கணக்கு அமைப்புகள் வெற்றிகரமாக
மாற்றப்பட்டுள்ளன. நீங்கள் மீண்டும் உள்நுழைய வேண்டும்.
show_subscriptions: சந்தாக்களைக் காட்டு
subscription_sort: வரிசைப்படுத்து
alphabetically: அகரவரிசை
subscriptions_in_own_sidebar: தனி பக்கப்பட்டியில்
sidebars_same_side: ஒரே பக்கத்தில் பக்கப்பட்டிகள்
subscription_sidebar_pop_out_right: வலதுபுறத்தில் பக்கப்பட்டியை பிரிக்க
நகர்த்தவும்
subscription_sidebar_pop_out_left: இடதுபுறத்தில் பக்கப்பட்டியை பிரிக்க
நகர்த்தவும்
subscription_sidebar_pop_in: சந்தாக்களை இன்லைன் பேனலுக்கு நகர்த்தவும்
subscription_panel_large: பெரிய குழு
subscription_header: சந்தா பத்திரிகைகள்
close: மூடு
position_bottom: கீழே
position_top: மேலே
pending: நிலுவையில் உள்ளது
flash_thread_new_error: நூலை உருவாக்க முடியவில்லை. ஏதோ தவறு நடந்தது.
flash_thread_tag_banned_error: நூலை உருவாக்க முடியவில்லை. உள்ளடக்கம்
அனுமதிக்கப்படவில்லை.
flash_image_download_too_large_error: படத்தை உருவாக்க முடியவில்லை, இது மிகப்
பெரியது (அதிகபட்ச அளவு %பைட்டுகள் %)
flash_email_was_sent: மின்னஞ்சல் வெற்றிகரமாக அனுப்பப்பட்டுள்ளது.
flash_email_failed_to_sent: மின்னஞ்சலை அனுப்ப முடியவில்லை.
flash_post_new_success: இடுகை வெற்றிகரமாக உருவாக்கப்பட்டுள்ளது.
flash_post_new_error: இடுகையை உருவாக்க முடியவில்லை. ஏதோ தவறு நடந்தது.
flash_magazine_theme_changed_success: செய்தித் தாள் தோற்றத்தை வெற்றிகரமாக
புதுப்பித்தது.
flash_magazine_theme_changed_error: செய்தித் தாள் தோற்றத்தை புதுப்பிக்கத்
தவறிவிட்டது.
flash_comment_new_success: கருத்து வெற்றிகரமாக உருவாக்கப்பட்டுள்ளது.
flash_comment_edit_success: கருத்து வெற்றிகரமாக புதுப்பிக்கப்பட்டுள்ளது.
flash_comment_new_error: கருத்தை உருவாக்கத் தவறிவிட்டது. ஏதோ தவறு நடந்தது.
flash_comment_edit_error: கருத்தைத் திருத்தத் தவறிவிட்டது. ஏதோ தவறு நடந்தது.
flash_user_settings_general_success: பயனர் அமைப்புகள் வெற்றிகரமாக
சேமிக்கப்பட்டன.
flash_user_settings_general_error: பயனர் அமைப்புகளைச் சேமிப்பதில் தோல்வி.
flash_user_edit_profile_error: சுயவிவர அமைப்புகளை சேமிப்பதில் தோல்வி.
flash_user_edit_profile_success: பயனர் சுயவிவர அமைப்புகள் வெற்றிகரமாக
சேமிக்கப்பட்டன.
flash_user_edit_email_error: மின்னஞ்சலை மாற்றத் தவறிவிட்டது.
flash_user_edit_password_error: கடவுச்சொல்லை மாற்றுவதில் தோல்வி.
flash_thread_edit_error: நூலைத் திருத்தத் தவறிவிட்டது. ஏதோ தவறு நடந்தது.
flash_post_edit_error: இடுகையைத் திருத்தத் தவறிவிட்டது. ஏதோ தவறு நடந்தது.
flash_post_edit_success: போச்ட் வெற்றிகரமாக திருத்தப்பட்டுள்ளது.
page_width: பக்க அகலம்
page_width_max: அதிகபட்சம்
page_width_auto: தானி
page_width_fixed: சரி
filter_labels: வடிகட்டி லேபிள்கள்
auto: தானி
open_url_to_fediverse: அசல் முகவரி ஐத் திறக்கவும்
change_my_avatar: எனது அவதாரத்தை மாற்றவும்
edit_my_profile: எனது சுயவிவரத்தைத் திருத்தவும்
change_my_cover: எனது அட்டையை மாற்றவும்
account_settings_changed: உங்கள் கணக்கு அமைப்புகள் வெற்றிகரமாக மாற்றப்பட்டுள்ளன.
நீங்கள் மீண்டும் உள்நுழைய வேண்டும்.
magazine_deletion: செய்தித் தாள் நீக்குதல்
delete_magazine: பத்திரிகையை நீக்கு
restore_magazine: பத்திரிகையை மீட்டெடுங்கள்
purge_magazine: பர்ச் செய்தித் தாள்
magazine_is_deleted: செய்தித் தாள் நீக்கப்பட்டது. நீங்கள் மீட்டமைக்கலாம் இது 30 நாட்களுக்குள்.
suspend_account: கணக்கு இடைநீக்கம்
unsuspend_account: விரும்பத்தகாத கணக்கு
account_suspended: கணக்கு இடைநிறுத்தப்பட்டுள்ளது.
account_unsuspended: கணக்கு சந்தேகத்திற்கு இடமின்றி உள்ளது.
deletion: நீக்குதல்
user_suspend_desc: உங்கள் கணக்கை இடைநிறுத்துவது உங்கள் உள்ளடக்கத்தை உதாரணமாக
மறைக்கிறது, ஆனால் அதை நிரந்தரமாக அகற்றாது, நீங்கள் எந்த நேரத்திலும் அதை
மீட்டெடுக்கலாம்.
account_banned: கணக்கு தடைசெய்யப்பட்டுள்ளது.
account_unbanned: கணக்கு தடைசெய்யப்படவில்லை.
account_is_suspended: பயனர் கணக்கு இடைநிறுத்தப்பட்டுள்ளது.
remove_following: பின்வருவனவற்றை அகற்று
remove_subscriptions: சந்தாக்களை அகற்று
apply_for_moderator: மதிப்பீட்டாளருக்கு விண்ணப்பிக்கவும்
request_magazine_ownership: செய்தித் தாள் உரிமையை கோருங்கள்
cancel_request: கோரிக்கையை ரத்துசெய்
moderator_requests: மோட் கோரிக்கைகள்
abandoned: கைவிடப்பட்டது
ownership_requests: உரிமை கோரிக்கைகள்
accept: ஏற்றுக்கொள்
action: செயல்
user_badge_op: ஒப்
user_badge_admin: நிர்வாகி
user_badge_global_moderator: உலகளாவிய துணிவு
user_badge_moderator: மோட்
user_badge_bot: போட்
announcement: அறிவிப்பு
keywords: முக்கிய வார்த்தைகள்
deleted_by_moderator: நூல், இடுகை அல்லது கருத்து மதிப்பீட்டாளரால் நீக்கப்பட்டது
deleted_by_author: நூல், இடுகை அல்லது கருத்து ஆசிரியரால் நீக்கப்பட்டது
sensitive_warning: உணர்திறன் உள்ளடக்கம்
sensitive_toggle: முக்கியமான உள்ளடக்கத்தின் தெரிவுநிலையை மாற்றவும்
sensitive_show: காண்பிக்க சொடுக்கு செய்க
details: விவரங்கள்
sensitive_hide: மறைக்க சொடுக்கு செய்க
spoiler: இறக்கைத்தடை
all_time: எல்லா நேரமும்
show: காட்டு
hide: மறை
edited: திருத்தப்பட்டது
sso_registrations_enabled: ஒருகைஉள் பதிவுகள் இயக்கப்பட்டன
sso_registrations_enabled.error: மூன்றாம் தரப்பு அடையாள மேலாளர்களுடன் புதிய
கணக்கு பதிவுகள் தற்போது முடக்கப்பட்டுள்ளன.
restrict_magazine_creation: உள்ளக செய்தித் தாள் உருவாக்கத்தை நிர்வாகிகள் மற்றும்
உலகளாவிய மோட்சுக்கு கட்டுப்படுத்துங்கள்
sso_show_first: உள்நுழைவு மற்றும் பதிவு பக்கங்களில் ஒருகைஉள் ஐ முதலில் காட்டு
continue_with: தொடருங்கள்
reported_user: அறிவிக்கப்பட்ட பயனர்
reporting_user: புகாரளிக்கும் பயனர்
reported: அறிக்கை
sso_only_mode: உள்நுழைவு மற்றும் பதிவை ஒருகைஉள் முறைகளுக்கு மட்டுமே
கட்டுப்படுத்தவும்
related_entry: தொடர்புடைய
report_subject: பொருள்
own_report_rejected: உங்கள் அறிக்கை நிராகரிக்கப்பட்டது
own_report_accepted: உங்கள் அறிக்கை ஏற்றுக்கொள்ளப்பட்டது
report_accepted: ஒரு அறிக்கை ஏற்றுக்கொள்ளப்பட்டது
open_report: திறந்த அறிக்கை
cake_day: கேக் நாள்
own_content_reported_accepted: உங்கள் உள்ளடக்கத்தின் அறிக்கை
ஏற்றுக்கொள்ளப்பட்டது.
someone: யாரோ
back: பின்
magazine_log_mod_added: ஒரு மதிப்பீட்டாளரைச் சேர்த்துள்ளார்
magazine_log_entry_pinned: பின் செய்யப்பட்ட நுழைவு
magazine_log_mod_removed: ஒரு மதிப்பீட்டாளரை அகற்றியுள்ளது
magazine_log_entry_unpinned: அகற்றப்பட்ட பின் நுழைவு
last_updated: கடைசியாக புதுப்பிக்கப்பட்டது
and: மற்றும்
direct_message: நேரடி செய்தி
manually_approves_followers: பின்பற்றுபவர்களுக்கு கைமுறையாக ஒப்புதல் அளிக்கிறது
register_push_notifications_button: புச் அறிவிப்புகளுக்கு பதிவு செய்யுங்கள்
unregister_push_notifications_button: புச் பதிவை அகற்று
test_push_notifications_button: சோதனை புச் அறிவிப்புகள்
test_push_message: வணக்கம் உலகம்!
notification_title_new_comment: புதிய கருத்து
notification_title_removed_comment: ஒரு கருத்து அகற்றப்பட்டது
notification_title_edited_comment: ஒரு கருத்து திருத்தப்பட்டது
notification_title_mention: நீங்கள் குறிப்பிடப்பட்டீர்கள்
notification_title_new_reply: புதிய பதில்
notification_title_new_thread: புதிய நூல்
notification_title_removed_thread: ஒரு நூல் அகற்றப்பட்டது
notification_title_edited_thread: ஒரு நூல் திருத்தப்பட்டது
notification_title_ban: உங்களுக்கு தடை விதிக்கப்பட்டது
notification_title_message: புதிய நேரடி செய்தி
notification_title_new_post: புதிய இடுகை
notification_title_removed_post: ஒரு இடுகை அகற்றப்பட்டது
notification_title_edited_post: ஒரு இடுகை திருத்தப்பட்டது
notification_title_new_signup: ஒரு புதிய பயனர் பதிவு செய்யப்பட்டார்
notification_body_new_signup: பயனர் % உ % பதிவு செய்யப்பட்டுள்ளது.
notification_body2_new_signup_approval: அவர்கள் உள்நுழைவதற்கு முன்பு நீங்கள்
கோரிக்கையை அங்கீகரிக்க வேண்டும்
show_related_magazines: சீரற்ற பத்திரிகைகளைக் காட்டு
show_related_entries: சீரற்ற நூல்களைக் காட்டு
show_related_posts: சீரற்ற இடுகைகளைக் காட்டு
show_active_users: செயலில் உள்ள பயனர்களைக் காட்டு
notification_title_new_report: ஒரு புதிய அறிக்கை உருவாக்கப்பட்டது
magazine_posting_restricted_to_mods_warning: இந்த பத்திரிகையில் மோட்ச் மட்டுமே
நூல்களை உருவாக்க முடியும்
flash_posting_restricted_error: நூல்களை உருவாக்குவது இந்த பத்திரிகையில் உள்ள
மோட்களுக்கு மட்டுப்படுத்தப்பட்டுள்ளது, நீங்கள் ஒன்றல்ல
server_software: சேவையக மென்பொருள்
last_failed_contact: கடைசியாக தோல்வியுற்ற தொடர்பு
magazine_posting_restricted_to_mods: நூல் உருவாக்கத்தை மதிப்பீட்டாளர்களுக்கு
கட்டுப்படுத்துங்கள்
new_user_description: இந்த பயனர் புதியது ( % நாட்களுக்கு குறைவான நாட்களுக்கு
செயலில் உள்ளது % நாட்கள்)
new_magazine_description: இந்த செய்தித் தாள் புதியது ( % நாட்களுக்கும் குறைவான
நாட்களுக்கு செயலில் உள்ளது % நாட்கள்)
version: பதிப்பு
last_successful_deliver: கடைசி வெற்றிகரமான வழங்கல்
last_successful_receive: கடைசியாக வெற்றிகரமாக பெறுதல்
admin_users_active: செயலில்
admin_users_inactive: செயலற்றது
admin_users_suspended: இடைநீக்கம்
bookmark_add_to_list: '%பட்டியலில் புக்மார்க்கைச் சேர்க்கவும் %'
bookmark_remove_from_list: '%பட்டியலிலிருந்து புத்தகக்குறியை அகற்று'
bookmark_remove_all: அனைத்து புக்மார்க்குகளையும் அகற்று
bookmark_add_to_default_list: இயல்புநிலை பட்டியலில் புக்மார்க்கைச் சேர்க்கவும்
bookmark_lists: புக்மார்க்கு பட்டியல்கள்
bookmarks: புக்மார்க்குகள்
bookmarks_list: '%பட்டியலில் புக்மார்க்குகள் %'
count: எண்ணுங்கள்
is_default: இயல்புநிலை
bookmark_list_is_default: இயல்புநிலை பட்டியல்
bookmark_list_make_default: இயல்புநிலை செய்யுங்கள்
bookmark_list_create: உருவாக்கு
bookmark_list_create_placeholder: பெயரைத் தட்டச்சு செய்க ...
bookmark_list_create_label: பட்டியல் பெயர்
bookmarks_list_edit: புக்மார்க்கு பட்டியலைத் திருத்து
bookmark_list_edit: தொகு
bookmark_list_selected_list: தேர்ந்தெடுக்கப்பட்ட பட்டியல்
search_type_entry: நூல்கள்
search_type_post: மைக்ரோ பிளாக்ச்
select_user: ஒரு பயனரைத் தேர்வுசெய்க
new_users_need_approval: புதிய பயனர்கள் உள்நுழைவதற்கு முன்பு ஒரு நிர்வாகியால்
அங்கீகரிக்கப்பட வேண்டும்.
table_of_contents: உள்ளடக்க அட்டவணை
search_type_all: நூல்கள் + மைக்ரோ பிளாக்ச்
signup_requests: பதிவுபெறும் கோரிக்கைகள்
application_text: நீங்கள் ஏன் சேர விரும்புகிறீர்கள் என்பதை விளக்குங்கள்
signup_requests_header: பதிவுபெறும் கோரிக்கைகள்
signup_requests_paragraph: இந்த பயனர்கள் உங்கள் சேவையகத்தில் சேர
விரும்புகிறார்கள். அவர்களின் பதிவுபெறும் கோரிக்கையை நீங்கள் அங்கீகரிக்கும் வரை
அவர்களால் உள்நுழைய முடியாது.
email_application_rejected_body: உங்கள் ஆர்வத்திற்கு நன்றி, ஆனால் உங்கள்
பதிவுபெறும் கோரிக்கை மறுக்கப்பட்டுள்ளது என்பதை உங்களுக்குத் தெரிவிக்க
வருத்தப்படுகிறோம்.
email_application_pending: நீங்கள் உள்நுழைவதற்கு முன்பு உங்கள் கணக்கில் நிர்வாக
ஒப்புதல் தேவைப்படுகிறது.
email_verification_pending: நீங்கள் உள்நுழைவதற்கு முன்பு உங்கள் மின்னஞ்சல்
முகவரியை சரிபார்க்க வேண்டும்.
remove_user_avatar: அவதாரத்தை அகற்று
remove_user_cover: அட்டையை அகற்று
show_new_icons: புதிய சின்னங்களைக் காட்டு
show_users_avatars_help: பயனர் அவதார் படத்தைக் காண்பி.
show_magazines_icons_help: செய்தித் தாள் ஐகானைக் காண்பி.
show_thumbnails_help: சிறு படங்களைக் காட்டு.
viewing_one_signup_request: '%பயனர்பெயர் %மூலம் ஒரு பதிவுபெறும் கோரிக்கையை மட்டுமே
நீங்கள் பார்க்கிறீர்கள்'
open_signup_request: பதிவுபெறும் கோரிக்கையை திறக்கவும்
show_new_icons_help: புதிய பத்திரிகை/பயனருக்கான ஐகானைக் காட்டு (30 நாட்கள் அகவை
அல்லது புதியது)
oauth2.grant.user.bookmark.remove: புக்மார்க்குகளை அகற்று
oauth2.grant.user.bookmark: புக்மார்க்குகளைச் சேர்த்து அகற்றவும்
oauth2.grant.user.bookmark.add: புக்மார்க்குகளைச் சேர்க்கவும்
oauth2.grant.user.bookmark_list.delete: உங்கள் புக்மார்க்கு பட்டியல்களை
நீக்கவும்
show_magazine_domains: செய்தித் தாள் களங்களைக் காட்டு
front_default_sort: முன் பக்கம் இயல்புநிலை வரிசை
comment_default_sort: கருத்து இயல்புநிலை வரிசை
email_application_approved_body: உங்கள் பதிவுபெறும் கோரிக்கையை சேவையக நிர்வாகி
அங்கீகரித்தார். நீங்கள் இப்போது சேவையகத்தில் %sitename% இல் உள்நுழையலாம்.
toolbar.emoji: ஈமோசி
2fa.manual_code_hint: நீங்கள் QR குறியீட்டை வருடு செய்ய முடியாவிட்டால்,
கைமுறையாக ரகசியத்தை உள்ளிடவும்
oauth2.grant.user.bookmark_list: உங்கள் புத்தகக்குறி பட்டியல்களைப் படிக்கவும்,
திருத்தவும் நீக்கவும்
oauth2.grant.user.bookmark_list.read: உங்கள் புத்தகக்குறி பட்டியல்களைப்
படியுங்கள்
oauth2.grant.user.bookmark_list.edit: உங்கள் புத்தகக்குறி பட்டியல்களைத்
திருத்தவும்
flash_application_info: நீங்கள் உள்நுழைவதற்கு முன்பு ஒரு நிர்வாகி உங்கள் கணக்கை
அங்கீகரிக்க வேண்டும். உங்கள் பதிவுபெறும் கோரிக்கை செயலாக்கப்பட்டவுடன்
உங்களுக்கு மின்னஞ்சலைப் பெறுவீர்கள்.
email_application_approved_title: உங்கள் பதிவுபெறும் கோரிக்கை
அங்கீகரிக்கப்பட்டுள்ளது
email_application_rejected_title: உங்கள் பதிவுபெறும் கோரிக்கை
நிராகரிக்கப்பட்டுள்ளது
show_user_domains: பயனர் களங்களைக் காட்டு
by: மூலம்
image_lightbox_in_list: நூல் சிறுபடங்கள் முழுத் திரையைத் திறக்கும்
compact_view_help: குறைந்த ஓரங்களுடன் ஒரு சிறிய பார்வை, அங்கு ஊடகங்கள் வலது
பக்கத்திற்கு நகர்த்தப்படுகின்றன.
image_lightbox_in_list_help: சரிபார்க்கும்போது, சிறுபடத்தைக் சொடுக்கு செய்வது
ஒரு மாதிரி பட பெட்டி சாளரத்தைக் காட்டுகிறது. தேர்வு செய்யப்படும்போது,
சிறுபடத்தைக் சொடுக்கு செய்தால் நூலைத் திறக்கும்.
answered: பதில்அளிக்கப்பட்டது
================================================
FILE: translations/messages.tr.yaml
================================================
type.photo: Fotoğraf
type.video: Video
search: Ara
add: Ekle
newest: En Yeni
oldest: En Eski
login: Giriş yap
filter_by_time: Zamana göre filtrele
filter_by_type: Türe göre filtrele
favourites: Oylar
up_votes: Boostlar
add_comment: Yorum ekle
add_post: Gönderi ekle
add_media: Medya ekle
owner: Sahibi
subscribers: Aboneler
online: Çevrim İçi
comments: Yorumlar
more: Daha Fazla
avatar: Avatar
added: Eklendi
moderators: Moderatörler
mod_log: Moderasyon kaydı
enter_your_post: Gönderinizi girin
activity: Etkinlik
empty: Boş
microblog: Mikroblog
events: Olaylar
password: Şifre
remember_me: Beni hatırla
you_cant_login: Parolanızı mı unuttunuz?
already_have_account: Halihazırda hesabınız var mı?
register: Kaydol
reset_password: Şifreyi sıfırla
show_more: Daha fazlası
about_instance: Hakkında
fediverse: Fediverse
follow: Takip et
unfollow: Takipten çık
reply: Cevapla
email: E-posta
repeat_password: Şifreyi tekrarla
faq: SSS
rss: RSS
change_theme: Temayı değiştir
help: Yardım
check_email: E-postanızı kontrol edin
email_confirm_content: 'Mbin hesabınızı etkinleştirmeye hazır mısınız? Aşağıdaki bağlantıya
tıklayın:'
type.link: Bağlantı
select_channel: Bir kanal seç
markdown_howto: Editör nasıl çalışır?
enter_your_comment: Yorumunuzu girin
subscribe: Abone ol
dont_have_account: Hesabınız yok mu?
username: Kullanıcı adı
contact: İletişim
email_confirm_header: Merhaba! E-posta adresinizi doğrulayın.
type.smart_contract: Akıllı sözleşme
type.magazine: Magazin
people: İnsanlar
magazine: Magazin
magazines: Dergiler
active: Aktif
commented: Yorum yapıldı
change_view: Görünümü değiştir
comments_count: '{0}Yorum|{1}Yorum|]1,Inf[ Yorumlar'
favourite: Favori
down_votes: Azaltır
no_comments: Yorum yok
created_at: Oluşturuldu
posts: Gönderiler
replies: Cevaplar
cover: Kapak
related_posts: İlgili gönderiler
random_posts: Rasgele gönderiler
unsubscribe: Abonelikden çık
login_or_email: Giriş veya e-posta
terms: Kullanım Şartları
privacy_policy: Gizlilik Politikası
all_magazines: Tüm dergiler
stats: İstatistik
create_new_magazine: Yeni dergi oluştur
add_new_link: Yeni bağlantı ekle
add_new_photo: Yeni fotoğraf ekle
add_new_post: Yeni gönderi ekle
add_new_video: Yeni video ekle
useful: Kullanışlı
reset_check_email_desc: Halihazırda e-posta adresinizle ilişkilendirilmiş bir
hesap varsa, kısa süre içinde parolanızı sıfırlamak için kullanabileceğiniz
bir bağlantı içeren bir e-posta alacaksınız. Bu bağlantı %expire% içinde
geçerliliğini yitirecek.
reset_check_email_desc2: Bir e-posta almazsanız, lütfen spam klasörünüzü kontrol
edin.
try_again: Yeniden dene
up_vote: Yükselt
url: URL
eng: ENG
oc: Oİ
image: Resim
name: İsim
description: Tanım
is_adult: +18 / NSFW
down_vote: Azalt
email_verify: E-posta adresini onayla
email_confirm_expire: Lütfen bağlantının bir saat içinde geçerliliğini
kaybedeceğini unutmayın.
select_magazine: Dergi seçin
add_new: Yenisini ekle
title: Başlık
tags: Etiketler
badges: Rozetler
email_confirm_title: E-posta adresinizi onaylayın.
subscriptions: Abonelikler
overview: Genel bakış
cards: Kartlar
columns: Sütunlar
user: Kullanıcı
moderated: Yönetilenler
people_local: Yerel
related_tags: İlgili etiketler
go_to_content: İçeriğe git
go_to_filters: Filtrelere git
go_to_search: Aramaya git
subscribed: Abone olundu
all: Hepsi
classic_view: Klassik görünüm
compact_view: Kompakt görünüm
chat_view: Sohbet görünümü
tree_view: Ağaç görünümü
table_view: Tablo görünümü
3h: 3 saat
6h: 6 saat
12h: 12 saat
1d: 1 gün
1y: 1 yıl
links: Bağlantılar
photos: Fotoğraflar
videos: Videolar
report: Raporla
share: Paylaş
copy_url_to_fediverse: Bağlantıyı Fediverse'e kopyala
share_on_fediverse: Fediverse'de paylaş
edit: Düzenle
are_you_sure: Emin misin?
moderate: Yönet
reason: Sebep
delete: Sil
edit_post: Gönderiyi düzenle
edit_comment: Yorumu düzenle
settings: Ayarlar
general: Genel
profile: Profil
blocked: Engelli
reports: Raporlar
notifications: Bildirimler
messages: Mesajlar
homepage: Ana sayfa
hide_adult: Yetişkin içeriği gizle
featured_magazines: Öne çıkan dergiler
privacy: Gizlilik
show_profile_followings: İzlenilen kullanıcıları göster
old_email: Mevcut e-posta
new_email: Yeni e-posta
current_password: Şimdiki şifre
new_password: Yeni şifre
new_password_repeat: Yeni şifreyi onayla
change_email: E-postayı değiştir
change_password: Şifreyi değiştir
expand: Genişlet
error: Hata
votes: Oylar
theme: Tema
dark: Karanlık
light: Aydınlık
font_size: Yazı boyutu
size: Boyut
yes: Evet
no: Hayır
show_thumbnails: Küçük resimleri göster
rounded_edges: Yuvarlak kenarlar
subject_reported: İçerik rapor edildi.
show_top_bar: Üst çubuğu göster
infinite_scroll: Sonsuz kaydırma
message: Mesaj
send_message: Mesaj gönder
purge: Tamamen sil
post: Gönderi
comment: Yorum
mentioned_you: Sizden bahsetti
deleted: Yazar tarafından silindi
banned: Sizi yasakladı
added_new_reply: Yeni cevap ekledi
mod_remove_your_post: Bir yönetici sizin gönderinizi kaldırdı
removed: Yönetici tarafından kaldırıldı
edited_post: Gönderi düzenledi
added_new_post: Yeni gönderi eklendi
replied_to_your_comment: Yorumunuza cevap verdi
edited_comment: Bir yorum düzenlendi
added_new_comment: Yeri yorum ekledi
registration_disabled: Kayıt devre dışı
registrations_enabled: Kayıt etkinleştirildi
type_search_term: Arama terimini yaz
FAQ: SSS
pages: Sayfalar
instance: Örnek
dashboard: Gösterge Paneli
admin_panel: Yönetici paneli
local: Yerel
year: Yıl
months: Aylar
month: Ay
weeks: Haftalar
week: Hafta
content: İçerik
users: Kullanıcılar
writing: Yazı
note: Not
reputation: İtibar
pinned: Sabitlendi
change: Değiştir
change_language: Dili değiştir
change_magazine: Dergi değiştir
unpin: Sabitlemeyi kaldır
pin: Sabitle
done: Oldu
icon: Simge
trash: Çöp
perm: Kalıcı
expires: Süresi doluyor
created: Oluşturuldu
bans: Yasaklar
add_badge: Rozet ekle
add_moderator: Yönetici ekle
rejected: Reddedilmiş
filters: Filtreler
ban: Yasakla
approve: Onayla
reject: Reddet
magazine_panel: Dergi paneli
from_url: URL'den
upload_file: Dosya yükle
instances: Örnekler
status: Durum
right: Sağ
left: Sol
boost: Yükselt
return: Geri dön
kbin_intro_title: Fediverse'ü keşfet
dynamic_lists: Dinamik listeler
auto_preview: Otomatik medya önizleme
random_magazines: Rasgele dergiler
related_magazines: İlgili dergiler
ban_account: Hesabı yasakla
unban_account: Hesap yasağını kaldır
purge_account: Hesabı tamamen sil
delete_account: Hesabı sil
active_users: Aktif insanlar
send: Gönder
firstname: Ad
Your account has been banned: Hesabınız yasaklandı.
Your account is not active: Heabınız aktif değil.
Password is invalid: Şifre yanlış.
followers: Takipçiler
restore: Onar
following: Takip edilenler
contact_email: E-posta aracılığıyla iletişme geç
reputation_points: İtibar puanları
preview: Ön izleme
logout: Çıkış yap
expired_at: Süresinin dolma zamanı
cards_view: Kart görünümü
approved: Onaylanmış
copy_url: Mbin bağlantıyı kopyala
ban_expired: Yasak süresi doldu
wrote_message: Mesaj yazdı
mod_deleted_your_comment: Bir yönetici sizin yorumunuzu sildi
appearance: Görünüm
mod_log_alert: UYARI - Modlog'da yöneticiler tarafından kaldırılmış önemli
gönderiler bula bilirsiniz. Ne yaptığınızı bildiğinizden emin olun.
show_profile_subscriptions: Dergi aboneliklerini göster
new_email_repeat: Yeni e-postayı onayla
show_users_avatars: Kullanıcı avatarını göster
body: Gövde
rules: Kurallar
notify_on_new_post_reply: Gönderilerimdeki bütün cevaplar
notify_on_new_post_comment_reply: Gönderilerdeki yorumlarıma gelen cevaplar
he_banned: ban
he_unbanned: yasağı kaldır
show_all: Hepsini göster
about: Hakkında
too_many_requests: Limit aşıldı, lütfen daha sonra tekrar deneyiniz.
flash_thread_edit_success: Başlık başarıyla düzenlendi.
flash_thread_delete_success: Başlık başarıyla silindi.
flash_thread_pin_success: Başlık başarıyla sabitlendi.
flash_thread_unpin_success: Başlığın sabitlenmesi başarıyla kaldırıldı.
set_magazines_bar_empty_desc: Alan boş ise aktif dergiler bar üzerinde
gösterilir.
flash_register_success: Aramıza hoş geldin! Hesabın başarıyla kaydedildi. Son
bir adım kaldı! - Gelen kutunu kontrol et, hesabını aktif hale getirecek bir
aktivasyon bağlantısı gönderdik.
flash_thread_new_success: Başlık başarıyla oluşturuldu ve artık diğer
kullanıcılara görünebilir durumda.
flash_magazine_new_success: Magazin başarıyla oluşturuldu. Artık yeni içerik
ekleyebilirsiniz veya magazinin yonetici panelini keşfedebilirsiniz.
type.article: İçerik
thread: Başlık
threads: Başlıklar
top: Üst
hot: Sıcak
federated_magazine_info: Bu magazin federe bir sunucudan geliyor ve eksik
olabilir.
federated_user_info: Bu profil federe bir sunucudan geliyor ve eksik olabilir.
go_to_original_instance: Özgün oluşuma daha fazla göz atın.
agree_terms: '%terms_link_start%Kullanım şartlarını%terms_link_end% ve %policy_link_start%Gizlilik
Politikasını%policy_link_end% onayla'
add_new_article: Yeni başlık ekle
domain: Alan
joined: Katılanlar
people_federated: Birleştirilmiş
notify_on_new_entry_reply: Başlıklarımdaki bütün yorumlar
notify_on_new_entry_comment_reply: Herhangi bir başlıkta yorumlarıma gelen
yanıtlar
notify_on_new_entry: Abone olduğum magazinlerdeki yeni başlıklar (bağlantılar
veya makaleler)
notify_on_new_posts: Abone olduğum magazinlerdeki yeni gönderiler
save: Kayıt et
collapse: Daralt
domains: Alanlar
boosts: Boost'lar
show_magazines_icons: Dergilerin simgelerini göster
restored_thread_by: 'tarafından açılan başlık canlandırıldı:'
removed_thread_by: 'tarafından açılan başlık kaldırıldı:'
removed_comment_by: 'tarafından paylaşılan yorum kaldırıldı:'
restored_comment_by: 'tarafından paylaşılan yorum canlandırıldı:'
removed_post_by: 'tarafından paylaşılan gönderi kaldırıldı:'
restored_post_by: 'tarafından paylaşılan gönderi canlandırıldı:'
read_all: Hepsini oku
flash_magazine_edit_success: Magazin başarılı bir şekilde düzenlendi.
set_magazines_bar: Derginin bar'ları
set_magazines_bar_desc: virgülden sonra magazin isimlerini ekle
articles: Başlıklar
sidebar_position: Yanbar pozisyonu
federation: Federasyon
add_ban: Yasaklama ekle
image_alt: Resim alternatif metni
1m: 1 ay
1w: 1 hafta
added_new_thread: Yeni konu eklendi
edited_thread: Konu düzenlendi
mod_remove_your_thread: Bir yönetici konunuzu kaldırdı
sticky_navbar: Sabit menü çubuğu
federated: Birleştirilmiş
federation_enabled: Federasyon aktifleştirildi
add_mentions_posts: Gönderilere bahsetme etiketi ekle
captcha_enabled: Captcha aktifleştirildi
sidebar: Yan bar
random_entries: Rastgele konular
related_entries: Alakalı konular
browsing_one_thread: Tartışmadaki sadece bir konuya bakıyorsunuz! Tüm yorumlar
gönderi sayfasında mevcut.
kbin_promo_desc: "%link_start%Repo'yu klonlayın%link_end% ve Fediverse'ı geliştirin"
article: Başlık
kbin_intro_desc: Fediverse ağı içinde işleyen ve merkezi olmayan, içerik
biriktirme ve mikrobloglama platformudur.
to: ile
in: içinde
solarized_light: Solarize Işık
solarized_dark: Solarize Karanlık
on: Açık
off: Kapalı
meta: Meta
add_mentions_entries: Başlıklara bahsetme etiketi ekle
banned_instances: Engellenmiş sunucular
magazine_panel_tags_info: Bu bilgiyi sadece fediverse'ten etiketler aracılığıyla
içerik gelmesini istiyorsanız girin
kbin_promo_title: Kendi sunucunu oluştur
header_logo: Header logosu
mercure_enabled: Merkür etkinleştir
report_issue: Sorun bildir
tokyo_night: Tokyo Gecesi
infinite_scroll_help: Sayfanın en altına ulaştığınızda otomatik olarak daha
fazla içerik yükleyin.
sticky_navbar_help: Navigasyon paneli aşağı kaydırdığınızda sayfanın üst kısmına
yapışacaktır.
auto_preview_help: Medya önizlemelerini otomatik olarak genişletin.
reload_to_apply: Değişiklikleri uygulamak için sayfayı yeniden yükleyin
preferred_languages: Başlıkların ve gönderilerin dillerini filtreleyin
filter.adult.show: NSFW Göster
bot_body_content: "Mbin Botuna hoş geldiniz! Bu bot, ActivityPub işlevselliğini Mbin
içinde etkinleştirmede çok önemli bir rol oynar. Bu, Mbin ’in fediverse’deki diğer
örneklerle iletişim kurabilmesini ve federasyon kurabilmesini sağlar.\n\nActivityPub,
Merkezi olmayan sosyal ağ platformlarının birbirleriyle iletişim kurmasını ve etkileşimde
bulunmasını sağlayan açık standart bir protokoldür. Farklı örneklerdeki (sunucular)
kullanıcıların Fediverse olarak bilinen birleşik sosyal ağdaki içeriği takip etmelerini,
etkileşimde bulunmalarını ve paylaşmalarını sağlar. Kullanıcıların içerik yayınlamaları,
diğer kullanıcıları takip etmeleri ve konuları veya gönderileri beğenme, paylaşma
ve yorum yapma gibi sosyal etkileşimlere katılmaları için standart bir yol sağlar."
filter.origin.label: Kaynak seç
filter.fields.label: Hangi alanları aracağınızı seçin
filter.adult.label: NSFW’nin görüntülenip görüntülenmeyeceğini seçin
filter.adult.hide: NSFW Gizle
filter.adult.only: Sadece NSFW
local_and_federated: Yerel ve federe
filter.fields.only_names: Sadece isimler
filter.fields.names_and_descriptions: İsimler ve açıklamalar
kbin_bot: Mbin Bot
password_confirm_header: Parola değiştirme isteğinizi onaylayın.
sort_by: Göre sırala
filter_by_subscription: Aboneliğe göre filtrele
filter_by_federation: Federasyon durumuna göre filtrele
subscribers_count: '{0}Aboneler|{1}Abone|]1,Bilgi[Aboneler'
followers_count: '{0}Takipçiler|{1}Takipçi|]1,Bilg[ Takipçiler'
marked_for_deletion: Silinmek üzere işaretlendi
marked_for_deletion_at: '%date% tarihinde silinmek üzere işaretlendi'
remove_media: Medyayı kaldır
remove_user_avatar: Avatarı kaldır
remove_user_cover: Kapağı kaldır
disconnected_magazine_info: Bu dergi güncelleme almıyor (son etkinlik %days% gün
önce).
always_disconnected_magazine_info: Bu dergiye güncelleme gelmiyor.
subscribe_for_updates: Güncellemeleri almaya başlamak için abone olun.
change_downvotes_mode: Oylama modunu değiştir
hidden: Gizlenmiş
enabled: Etkinleştirilmiş
tag: Etiket
================================================
FILE: translations/messages.uk.yaml
================================================
filter_by_type: Фільтр за типом
2fa.authentication_code.label: Код автентифікації
comment: Коментар
size: Розмір
oauth2.grant.post.edit: Редагувати ваші наявні дописи.
already_have_account: Вже є обліковий запис?
oauth2.grant.moderate.post.trash: Видаляти або відновлювати дописи у ваших
модерованих спільнотах.
moderation.report.approve_report_title: Прийняти скаргу
preview: Попередній перегляд
moderation.report.reject_report_title: Відхилити скаргу
kbin_bot: Mbin Бот
dashboard: Панель керування
added_new_reply: Додає нову відповідь
bans: Заборонені
deleted: Видалено автором
oauth2.grant.moderate.magazine.reports.all: Управляти скаргами у ваших
модерованих спільнотах.
reputation_points: Бали репутації
oauth2.grant.admin.federation.update: Додавати або видаляти інстанси у список/зі
списку дефедерованих.
featured_magazines: Рекомендовані спільноти
mod_remove_your_thread: Модератор видаляє вашу гілку
filter.adult.label: Виберіть, чи відображати делікатний вміст
resend_account_activation_email_error: Під час надсилання цього запиту виникла
проблема. Можливо, з цією е-поштою не повʼязано облікового запису або він уже
активований.
federation_page_enabled: Сторінку федерації ввімкнено
share_on_fediverse: Поділитися у Федіверс
federated_magazine_info: Спільнота з федерованого сервера, може відображатися не
повністю.
month: Місяць
reset_check_email_desc: Якщо з вашою адресою е-пошти вже повʼязано обліковий
запис, незабаром ви отримаєте електронного листа з посиланням, за яким можна
скинути пароль. Посилання стане недійсним за %expire%.
oauth2.grant.user.message.all: Читати ваші повідомлення та надсилати
повідомлення іншим користувачам.
flash_account_settings_changed: Налаштування вашого облікового запису успішно
змінено. Вам потрібно буде увійти ще раз.
close: Закрити
set_magazines_bar_empty_desc: якщо це поле порожнє, на панелі відображатимуться
активні спільноти.
reply: Відповісти
down_vote: Невподобати
following: Відстежувані
top: Провідні
reports: Скарги
oauth2.grant.moderate.magazine.trash.read: Переглядати видалений вміст у ваших
модерованих спільнотах.
show_thumbnails: Показувати мініатюри
email_confirm_button_text: Необхідно підтвердити запит на зміну пароля
font_size: Розмір шрифту
save: Зберегти
writing: Написання
change_view: Змінити вигляд
oauth2.grant.moderate.magazine_admin.create: Створювати нові спільноти.
weeks: Тижні
filter.adult.hide: Сховати делікатний вміст
oauth2.grant.post.vote: Голосувати за, проти або поширювати будь-який допис.
subscribe: Підписатися
FAQ: ЧаП
2fa.remove: Видалити 2FA
created_at: Створено
delete_content_desc: Видалити вміст користувача, залишивши відповіді інших
користувачів у створених гілках, дописах і коментарях.
magazine_theme_appearance_custom_css: Власний CSS для застосування під час
перегляду вашої спільноти відвідувачами.
votes: Голоси
title: Заголовок
flash_post_new_success: Публікація успішно створена.
copy_url: Скопіювати локальне посилання
moderators: Модератори
flash_comment_edit_error: Не вдалося відредагувати коментар. Щось пішло не так.
toolbar.bold: Жирний
errors.server429.title: 429 Забагато запитів
auto_preview_help: Показати попередній перегляд медіафайлів (фото, відео) у
збільшеному розмірі під вмістом.
filter.fields.label: Виберіть поля для пошуку
are_you_sure: Ви впевнені?
2fa.backup_codes.help: 'Скористайтеся цими кодами, якщо у вас не буде пристрою двофакторної
автентифікації або програми. Збережіть їх просто зараз , оскільки
вони більше ніколи не відображатимуться. Памʼятайте: ви зможете використати кожен
із них лише один раз .'
federation: Федерація
thread: Гілка
toolbar.header: Заголовок
cards: Картки
comments_count: '{0}коментарів|{1}коментар|]1,Inf[ коментарів'
Your account is not active: Ваш обліковий запис не активний.
flash_comment_new_error: Не вдалося створити коментар. Щось пішло не так.
you_cant_login: Забули свій пароль?
password: Пароль
oauth2.grant.user.oauth_clients.edit: Редагувати дозволи, які ви надали іншим
програмам OAuth2.
mentioned_you: Згадує вас
user: Користувач
oauth2.grant.user.all: Читати і редагувати ваш профіль, повідомлення чи
сповіщення; читати і редагувати дозволи, які ви надали іншим програмам;
відстежувати або блокувати інших користувачів; переглядати списки
користувачів, яких ви відстежуєте або блокуєте.
oauth2.grant.moderate.post.set_adult: Позначати дописи як «Делікатне» у ваших
модерованих спільнотах.
random_posts: Випадкові дописи
oauth2.grant.moderate.magazine_admin.edit_theme: Редагувати користувацький CSS
будь-якої вашої спільноти.
oauth2.grant.moderate.magazine_admin.tags: Створювати або видаляти теги з ваших
спільнот.
subscribed: Підписане
send_message: Надіслати повідомлення
add_new: Додати
trash: Кошик
moderation.report.ban_user_description: Бажаєте заборонити користувачу
(%username%), який створив цей вміст, доступ до цієї спільноти?
flash_thread_unpin_success: Гілку успішно відкріплено.
oauth2.grant.moderate.entry.pin: Закріплювати гілки вгорі у ваших модерованих
спільнотах.
1w: 1 тиждень
oauth2.grant.user.message.read: Читати ваші повідомлення.
flash_post_new_error: Не вдалося створити публікацію. Щось пішло не так.
message: Повідомлення
oauth2.grant.admin.entry.purge: Повністю видаляти будь-яку гілку з вашого
інстансу.
oldest: Старі
fediverse: Федіверс
2fa.verify: Підтвердити
cards_view: Вигляд карток
change_password: Змінити пароль
2fa.add: Додати до мого облікового запису
oauth.consent.to_allow_access: Щоб дозволити цей доступ, натисніть кнопку
«Дозволити» нижче
email_verify: Підтвердити адресу е-пошти
type.link: Посилання
oauth2.grant.admin.magazine.all: Переміщати гілки між спільнотами або повністю
видаляти спільноти на вашому інстансі.
filter.fields.only_names: Тільки назви
copy_url_to_fediverse: Скопіювати пряме посилання
show_top_bar: Показувати верхню панель
favourites: Уподобання
read_all: Усі прочитані
notify_on_new_posts: Нові дописи в будь-якій спільноті, на яку ви підписані
oauth2.grant.admin.instance.settings.read: Переглядати налаштування на вашому
інстансі.
blocked: Заблоковане
page_width_fixed: Фіксовано
oauth2.grant.entry.report: Скаржитися на будь-яку гілку.
oauth2.grant.moderate.post_comment.all: Модерувати коментарі до дописів у ваших
модерованих спільнотах.
rss: RSS
removed_thread_by: видаляє гілку, створену
2fa.disable: Вимкнути двофакторну автентифікацію
local_and_federated: Місцеві та федеративні
email.delete.description: Наступний користувач подав запит на видалення свого
облікового запису
flash_thread_new_error: Не вдалося створити гілку. Щось пішло не так.
last_active: Остання активність
purge_account: Повністю видалити обліковий запис
show_profile_subscriptions: Показувати підписки на спільноти
delete_account: Видалити обліковий запис
ban: Заборонити
flash_thread_edit_success: Гілку успішно відредаговано.
added_new_comment: Додає новий коментар
restored_post_by: відновлює допис, створений
purge_content: Повністю видалити вміст
oauth2.grant.domain.subscribe: Підписуватись, відписуватись, а також переглядати
домени, на які ви підписані.
solarized_light: Solarized Світла
sticky_navbar: Закріплена навігаційна панель
magazine_theme_appearance_icon: Власний значок для спільноти. Якщо не вибрано
жодного, буде використано значок за замовчуванням.
toolbar.ordered_list: Упорядкований список
kbin_intro_title: Досліджуйте Федіверс
microblog: Мікроблог
email_confirm_content: 'Готові активувати свій обліковий запис Mbin? Натисніть на
посилання нижче:'
oauth2.grant.moderate.magazine.ban.create: Забороняти користувачів у ваших
модерованих спільнотах.
firstname: Імʼя
sidebar_position: Положення бічної панелі
oauth2.grant.admin.user.delete: Видаляти користувачів з вашого інстансу.
oauth.consent.app_requesting_permissions: хоче виконати наступні дії від вашого
імені
no: Ні
oauth2.grant.moderate.post.change_language: Змінювати мову дописів у ваших
модерованих спільнотах.
oauth2.grant.moderate.magazine_admin.delete: Видаляти будь-які ваші спільноти.
unpin: Відкріпити
oauth2.grant.moderate.entry_comment.all: Модерувати коментарі в гілках у ваших
модерованих спільнотах.
flash_magazine_theme_changed_error: Не вдалося оновити зовнішній вигляд
спільноти.
add_badge: Додати значок
oauth2.grant.admin.magazine.move_entry: Переміщати гілки між спільнотами на
вашому інстансі.
oauth2.grant.post_comment.edit: Редагувати ваші наявні коментарі до дописів.
flash_post_edit_error: Не вдалося відредагувати публікацію. Щось пішло не так.
all: Усе
tags: Теги
oauth2.grant.moderate.post.pin: Закріплювати дописи вгорі у ваших модерованих
спільнотах.
change: Змінити
videos: Відео
icon: Значок
new_password: Новий пароль
newest: Нові
oauth2.grant.entry.create: Створювати нові гілки.
federated_search_only_loggedin: Федерований пошук обмежено, якщо ви не ввійшли
reason: Причина
page_width_auto: Автоматично
set_magazines_bar_desc: введіть назви спільнот, розділивши їх комами
select_channel: Показати
tree_view: Вигляд дерева
followers: Відстежувачі
type.photo: Зображення
oauth2.grant.moderate.magazine.reports.action: Приймати і відхиляти скарги у
ваших модерованих спільнотах.
online: У мережі
solarized_dark: Solarized Темна
activity: Залученість
oauth2.grant.admin.magazine.purge: Повністю видаляти спільноти на вашому
інстансі.
add_post: Додати допис
related_tags: Повʼязані теги
hot: Гарячі
your_account_is_not_active: Ваш обліковий запис не було активовано. Перевірте
свою е-пошту, щоб отримати інструкції щодо активації облікового запису, або надішліть запит на новий електронний лист для активації
облікового запису.
image_alt: Опис зображення
flash_post_edit_success: Публікація успішно відредагована.
oauth2.grant.user.notification.read: Читати ваші сповіщення, у тому числі
сповіщення про повідомлення.
meta: Метаінформація
right: Праворуч
on: Увімк.
email: Е-пошта
filter.adult.show: Показувати делікатний вміст
edit: Редагувати
oc: ОВ
restore: Відновити
flash_user_edit_password_error: Не вдалося змінити пароль.
unfollow: Не відстежувати
cover: Обкладинка
oauth.consent.allow: Дозволити
subscribers: Підписники
oauth2.grant.magazine.block: Блокувати або розблокувати спільноти та переглядати
спільноти, які ви заблокували.
position_bottom: Кнопка
type.magazine: Спільнота
people_local: Місцеві
show_more: Показати більше
oauth2.grant.magazine.subscribe: Підписуватись, відписуватись, а також
переглядати спільноти, на які ви підписалися.
down_votes: Невподобання
oauth2.grant.admin.user.all: Забороняти, перевіряти або повністю видаляти
користувачів на вашому інстансі.
2fa.backup_codes.recommendation: Рекомендуємо зберігати копію цих кодів у
безпечному місці.
notify_on_new_post_comment_reply: Відповіді на ваші коментарі до будь-яких
дописів
theme: Тема
login_or_email: Імʼя користувача або е-пошта
from_url: З вебадреси
add_new_link: Додати нове посилання
added: Додано
year: Рік
oauth2.grant.post_comment.report: Скаржитися на будь-який коментар до допису.
oauth2.grant.magazine.all: Підписуватись, блокувати, а також переглядати
спільноти, на які ви підписалися або заблокували.
new_password_repeat: Підтвердити новий пароль
oauth2.grant.vote.general: Голосувати за, проти або поширювати гілку, допис чи
коментар.
related_magazines: Схожі спільноти
appearance: Вигляд
remember_me: Запамʼятати мене
custom_css: Користувацький CSS
oauth2.grant.entry.vote: Голосувати за, проти або поширювати будь-яку гілку.
description: Опис
report_issue: Повідомити про помилку
oauth2.grant.moderate.magazine_admin.all: Створювати, редагувати або видаляти
ваші спільноти.
comment_reply_position_help: Розмістити форму відповіді на коментар вгорі або
внизу сторінки. Якщо «Нескінченна прокрутка» ввімкнена, форма завжди
відображатиметься вгорі.
name: Імʼя
filter.adult.only: Тільки делікатний вміст
current_password: Поточний пароль
in: в
yes: Так
oauth2.grant.post_comment.vote: Голосувати за, проти або поширювати будь-який
коментар до допису.
kbin_intro_desc: це децентралізована платформа для збору вмісту та ведення
мікроблогів, яка діє у мережі Федіверсу.
filter.fields.names_and_descriptions: Назви та описи
oauth2.grant.user.oauth_clients.read: Читати дозволи, які ви надали іншим
програмам OAuth2.
add_moderator: Додати модератора
oauth2.grant.moderate.entry.change_language: Змінювати мову гілок у ваших
модерованих спільнотах.
username: Імʼя користувача
password_confirm_header: Необхідно підтвердити запит на зміну пароля.
off: Вимк.
light: Світла
terms: Умови використання
compact_view: Компактний вигляд
type.article: Гілка
email_confirm_header: Вітаємо! Необхідно підтвердити вашу адресу е-пошти.
block: Блокувати
rejected: Відхилено
image: Зображення
table_view: Вигляд таблиці
oauth2.grant.moderate.all: Виконувати будь-яку дію модерації, на яку ви маєте
дозвіл, у ваших модерованих спільнотах.
help: Довідка
oauth2.grant.moderate.magazine.ban.all: Управляти заборонами у ваших модерованих
спільнотах.
create_new_magazine: Створити нову спільноту
people: Люди
oauth2.grant.moderate.magazine.all: Управляти заборонами, скаргами, а також
переглядати видалене у ваших модерованих спільнотах.
oauth2.grant.admin.federation.all: Переглядати й оновлювати дефедеровані наразі
інстанси.
12h: 12 годин
toolbar.quote: Цитата
oauth2.grant.user.notification.all: Читати й очищати ваші сповіщення.
sidebar: Бічна панель
filters: Фільтри
enter_your_post: Введіть ваш допис
oauth2.grant.report.general: Скаржитися на гілки, дописи чи коментарі.
oauth2.grant.moderate.magazine.list: Читати список ваших модерованих спільнот.
oauth2.grant.admin.post.purge: Повністю видаляти будь-який допис із вашого
інстансу.
flash_magazine_theme_changed_success: Вдалося оновити зовнішній вигляд
спільноти.
oauth2.grant.moderate.magazine.ban.read: Переглядати заборонених користувачів у
ваших модерованих спільнотах.
delete: Видалити
add_new_photo: Додати нове зображення
flash_thread_pin_success: Гілку успішно закріплено.
registration_disabled: Реєстрацію вимкнено
oauth2.grant.user.profile.all: Читати і редагувати ваш профіль.
oauth2.grant.admin.user.ban: Забороняти або допускати користувачів на вашому
інстансі.
1m: 1 місяць
show_avatars_on_comments: Показувати аватари в коментарях
oauth2.grant.admin.all: Виконувати будь-які адміністративні дії на вашому
інстансі.
select_magazine: Оберіть спільноту
toolbar.unordered_list: Невпорядкований список
to: до
errors.server404.title: 404 Не знайдено
expired_at: Закінчився
resend_account_activation_email_success: Якщо обліковий запис, повʼязаний із
цією е-поштою, існує, ми надішлемо нового електронного листа для активації.
register: Зареєструватися
pinned: Закріплене
errors.server403.title: 403 Заборонено
flash_magazine_new_success: Спільноту успішно створено. Тепер ви можете додати
новий вміст або дослідити панель адміністрування спільноти.
oauth2.grant.post.report: Скаржитися на будь-який допис.
oauth2.grant.moderate.magazine.reports.read: Читати скарги у ваших модерованих
спільнотах.
ignore_magazines_custom_css: Ігнорувати користувацький CSS спільнот
mercure_enabled: Mercure увімкнено
flash_thread_edit_error: Не вдалося відредагувати гілку. Щось пішло не так.
dont_have_account: Немає облікового запису?
set_magazines_bar: Панель спільнот
add_new_article: Додати нову гілку
rounded_edges: Заокруглені краї
article: Гілка
oauth2.grant.entry_comment.create: Створювати нові коментарі в гілках.
2fa.qr_code_img.alt: QR-код, який дозволяє налаштувати двофакторну
автентифікацію для вашого облікового запису
url: URL
sidebars_same_side: Бічні панелі на одній стороні
oauth.consent.deny: Заборонити
oauth2.grant.user.follow: Відстежувати або не відстежувати користувачів, а також
читати список користувачів, яких ви відстежуєте.
flash_user_edit_email_error: Не вдалося змінити електронну пошту.
flash_post_pin_success: Допис успішно закріплено.
send: Надіслати
active_users: Активні зараз
page_width_max: Максимум
faq: FAQ
banned: Забороняє вас
oauth2.grant.entry_comment.report: Скаржитися на будь-який коментар у гілці.
mod_log_alert: УВАГА! Журнал модерації може містити неприємний або тривожний
вміст, який було видалено модераторами. Будь ласка, будьте обережні.
moderation.report.approve_report_confirmation: Ви впевнені, що хочете прийняти
цю скаргу?
add_new_post: Додати новий допис
moderate: Модерувати
6h: 6 годин
body: Основний текст
oauth2.grant.moderate.entry.all: Модерувати гілки у ваших модерованих
спільнотах.
return: Повернутися
oauth.consent.title: Форма згоди OAuth2
contact: Звʼязок
open_url_to_fediverse: Відкрити оригінальну URL-адресу
federation_page_allowed_description: Відомі інстанси, з якими ми федеруємо
banned_instances: Заборонені інстанси
edited_thread: Редагує гілку
page_width: Ширина сторінки
resend_account_activation_email: Повторно надіслати електронного листа для
активації облікового запису
federated: Федерований
oauth2.grant.entry_comment.all: Створювати, редагувати або видаляти ваші
коментарі в гілках, а також голосувати, поширювати або скаржитися на будь-який
коментар у гілці.
notifications: Сповіщення
oauth2.grant.entry_comment.delete: Видаляти ваші наявні коментарі в гілках.
post: Допис
change_theme: Змінити тему
delete_account_desc: Видалити обліковий запис, залишивши відповіді інших
користувачів у створених гілках, дописах і коментарях.
instance: Інстанс
captcha_enabled: Капча увімкнена
perm: Назавжди
subject_reported_exists: На цей вміст уже подано скаргу.
flash_comment_new_success: Коментар успішно створено.
subject_reported: На цей вміст подано скаргу.
reset_check_email_desc2: Якщо ви не отримали електронного листа, перевірте теку
зі спамом.
add_new_video: Додати нове відео
dynamic_lists: Рухомі списки
rules: Правила
columns: Стовпці
oauth2.grant.moderate.entry_comment.set_adult: Позначати коментарі в гілках як
«Делікатне» у ваших модерованих спільнотах.
magazine_panel_tags_info: Вкажіть, лише якщо ви хочете, щоб вміст із Федіверсу
додавався до цієї спільноти на підставі тегів
oauth2.grant.entry.edit: Редагувати ваші наявні гілки.
moderated: Модероване
too_many_requests: Перевищено ліміт, будь ласка, спробуйте ще раз пізніше.
flash_user_settings_general_error: Не вдалося зберегти налаштування користувача.
oauth2.grant.moderate.entry_comment.trash: Видаляти або відновлювати коментарі в
гілках у ваших модерованих спільнотах.
add_mentions_entries: Автоматично додавати теги згадок у гілках
oauth2.grant.moderate.post.all: Модерувати дописи у ваших модерованих
спільнотах.
collapse: Згорнути
preferred_languages: Фільтрувати мови гілок і дописів
errors.server500.title: 500 Внутрішня помилка сервера
2fa.enable: Увімкнути двофакторну автентифікацію
oauth2.grant.entry_comment.edit: Редагувати ваші наявні коментарі в гілках.
threads: Гілки
about: Про вас
alphabetically: За алфавітом
auto_preview: Автоматичний перегляд медіа
up_votes: Поширення
local: Місцеві
2fa.user_active_tfa.title: Використовує 2FA
oauth2.grant.moderate.magazine_admin.update: Редагувати правила, опис, статус
«Делікатне» або значок будь-якої вашої спільноти.
removed_comment_by: видаляє коментар, створений
owner: Власник
wrote_message: Пише повідомлення
notify_on_new_entry_comment_reply: Відповіді на ваші коментарі в будь-яких
гілках
flash_email_was_sent: Лист успішно відправлено.
oauth2.grant.moderate.entry.trash: Видаляти або відновлювати гілки у ваших
модерованих спільнотах.
report: Поскаржитися
active: Активні
mod_deleted_your_comment: Модератор видаляє ваш коментар
status: Стан
new_email: Нова е-пошта
show_subscriptions: Показати підписки
privacy_policy: Політика приватності
oauth2.grant.user.oauth_clients.all: Читати і редагувати дозволи, які ви надали
іншим програмам OAuth2.
2fa.code_invalid: Код автентифікації недійсний
left: Ліворуч
mod_log: Журнал модерації
events: Події
oauth2.grant.user.profile.read: Читати ваш профіль.
toolbar.link: Посилання
oauth2.grant.admin.oauth_clients.read: Переглядати клієнти OAuth2, наявні на
вашому інстансі, а також статистику їх використання.
registrations_enabled: Реєстрацію увімкнено
toolbar.mention: Згадка
more: Більше
type_search_term: Введіть пошуковий запит
up_vote: Поширити
oauth2.grant.write.general: Створювати або редагувати будь-які ваші гілки,
дописи чи коментарі.
related_entries: Схожі гілки
try_again: Спробуйте знову
single_settings: Окреме
oauth2.grant.moderate.post_comment.set_adult: Позначати коментарі до дописів як
«Делікатне» у ваших модерованих спільнотах.
stats: Статистика
oauth2.grant.moderate.post_comment.change_language: Змінювати мову коментарів до
дописів у ваших модерованих спільнотах.
moderation.report.ban_user_title: Заборонити користувача
filter.origin.label: Виберіть походження
2fa.available_apps: Щоб відсканувати цей QR-код, використовуйте програму
двофакторної автентифікації, наприклад %google_authenticator%, %aegis%
(Android) чи %raivo% (iOS).
resend_account_activation_email_question: Неактивний обліковий запис?
unban_account: Допустити обліковий запис
random_magazines: Випадкові спільноти
links: Посилання
upload_file: Додати файл
oauth2.grant.admin.user.verify: Перевіряти користувачів на вашому інстансі.
dark: Темна
federation_enabled: Федерацію ввімкнено
flash_thread_new_success: Гілку успішно створено і тепер її бачать інші
користувачі.
go_to_search: Перейти до пошуку
restored_comment_by: відновлює коментар, створений
cancel: Скасувати
change_email: Змінити е-пошту
instances: Інстанси
random_entries: Випадкові гілки
markdown_howto: Як працює редактор?
go_to_filters: Перейти до фільтрів
reputation: Репутація
flash_comment_edit_success: Коментар успішно оновлено.
resend_account_activation_email_description: Введіть адресу е-пошти, повʼязану з
вашим обліковим записом. Ми надішлемо вам іншого електронного листа для
активації.
reload_to_apply: Перезавантажте сторінку, щоб застосувати зміни
notify_on_new_post_reply: Відповіді будь-якого рівня на ваші дописи
oauth2.grant.entry.delete: Видаляти ваші наявні гілки.
he_unbanned: допускає
your_account_has_been_banned: Ваш обліковий запис заборонено
oauth2.grant.admin.instance.information.edit: Оновлювати на вашому інстансі
сторінки «Про інстанс», «FAQ», «Звʼязок», «Умови використання» і «Політика
приватності».
oauth2.grant.read.general: Читати весь вміст, до якого ви маєте доступ.
oauth2.grant.domain.all: Підписуватись, блокувати, а також переглядати домени,
на які ви підписалися або заблокували.
reset_password: Скинути пароль
commented: Коментовані
toolbar.code: Код
errors.server500.description: Вибачте, в нас щось пішло не так. Ми працюємо над
розвʼязанням цієї проблеми, завітайте пізніше.
general: Загальні
subscriptions: Підписки
filter_by_time: Фільтр за часом
oauth.client_not_granted_message_read_permission: Ця програма не отримала
дозволу на читання ваших повідомлень.
add_mentions_posts: Автоматично додавати теги згадок у дописах
restrict_oauth_clients: Дозволити створення клієнта OAuth2 лише адміністраторам
email_confirm_title: Необхідно підтвердити вашу адресу е-пошти.
purge_content_desc: Повністю видалити вміст користувача, включаючи видалення
відповідей інших користувачів у створених гілках, дописах і коментарях.
flash_post_unpin_success: Допис успішно відкріплено.
oauth2.grant.post_comment.delete: Видаляти ваші наявні коментарі до дописів.
edit_post: Редагувати допис
no_comments: Немає коментарів
federation_page_disallowed_description: Інстанси, з якими ми не федеруємо
bot_body_content: "Ласкаво просимо до /kbin Бота! Цей бот відіграє вирішальну роль
в активації функціональності ActivityPub у Mbin. Він забезпечує Mbin спілкування
та федерацію з іншими інстансами у Федіверсі.\n\nActivityPub — це мережевий протокол
відкритого стандарту. Він дозволяє децентралізованим платформам соціальних мереж
спілкуватися та взаємодіяти одна з одною. Це дозволяє користувачам на різних інстансах
(серверах) стежити, взаємодіяти та ділитися вмістом у федеративній соціальній мережі,
відомій як Федіверс. Він надає користувачам стандартизований спосіб публікувати
вміст, відстежувати інших користувачів та брати участь у соціальних взаємодіях:
наприклад, уподобати, поширити чи коментувати гілки або дописи."
content: Вміст
joined: Приєднання
notify_on_new_entry: Нові гілки (посилання чи статті) в будь-якій спільноті, на
яку ви підписані
added_new_thread: Додає нову гілку
oauth2.grant.admin.oauth_clients.revoke: Відкликати доступ до клієнтів OAuth2 на
вашому інстансі.
oauth2.grant.admin.instance.settings.edit: Оновлювати налаштування на вашому
інстансі.
users: Користувачі
removed: Видалено модератором
domain: Домен
search: Шукати
go_to_original_instance: Переглянути на віддаленому інстансі.
oauth2.grant.moderate.entry.set_adult: Позначати гілки як «Делікатне» у ваших
модерованих спільнотах.
oauth2.grant.delete.general: Видаляти будь-які ваші гілки, дописи чи коментарі.
error: Помилка
oauth2.grant.entry_comment.vote: Голосувати за, проти або поширювати будь-який
коментар у гілці.
oauth2.grant.admin.instance.stats: Переглядати статистику вашого інстансу.
oauth2.grant.admin.instance.settings.all: Переглядати або оновлювати
налаштування на вашому інстансі.
logout: Вийти
oauth2.grant.entry.all: Створювати, редагувати або видаляти ваші гілки, а також
голосувати, поширювати або скаржитися на будь-яку гілку.
replies: Відповіді
add_ban: Заборонити
notify_on_new_entry_reply: Коментарі будь-якого рівня у гілках вашого авторства
delete_content: Видалити вміст
domains: Домени
two_factor_backup: Резервні коди двофакторної автентифікації
photos: Зображення
overview: Огляд
1y: 1 рік
classic_view: Класичний вигляд
ban_account: Заборонити обліковий запис
flash_user_edit_profile_success: Налаштування профілю користувача успішно
збережено.
subscription_sort: Сортування підписок
edit_comment: Зберегти зміни
expand: Розгорнути
go_to_content: Перейти до вмісту
messages: Повідомлення
magazine_theme_appearance_background_image: Власне зображення для тла вашої
спільноти.
login: Увійти
unblock: Розблокувати
show_profile_followings: Показувати відстежуваних
week: Тиждень
edited_comment: Редагує коментар
boost: Поширити
oauth2.grant.admin.federation.read: Переглядати список дефедерованих інстансів.
oauth2.grant.moderate.entry_comment.change_language: Змінювати мову коментарів у
гілках у ваших модерованих спільнотах.
mod_remove_your_post: Модератор видаляє ваш допис
oauth2.grant.moderate.magazine_admin.stats: Переглядати вміст, статистику
голосів і переглядів ваших спільнот.
password_and_2fa: Пароль і 2FA
flash_user_settings_general_success: Налаштування користувача успішно збережено.
1d: 1 день
oauth.consent.grant_permissions: Надати дозволи
infinite_scroll: Нескінченна прокрутка
empty: Пусто
ban_expired: Термін заборони —
change_language: Змінити мову
federated_user_info: Профіль із федерованого сервера, може відображатися не
повністю.
oauth2.grant.user.message.create: Надсилати повідомлення іншим користувачам.
oauth2.grant.admin.oauth_clients.all: Переглядати або відкликати клієнти OAuth2,
наявні на вашому інстансі.
flash_thread_delete_success: Гілку успішно видалено.
email_confirm_expire: 'Зверніть увагу: термін дії посилання закінчується за годину.'
browsing_one_thread: Ви переглядаєте лише одну гілку в обговоренні! Усі
коментарі доступні на сторінці допису.
people_federated: Федеровані
settings: Налаштування
pages: Сторінки
2fa.backup-create.label: Створити нові резервні коди автентифікації
magazines: Спільноти
oauth2.grant.moderate.magazine.ban.delete: Допускати користувачів у ваших
модерованих спільнотах.
2fa.backup: Ваші резервні коди автентифікації
pending: На розгляді
add_media: Додати медіа
useful: Корисне
subscriptions_in_own_sidebar: Підписки окремо
restored_thread_by: відновлює гілку, створену
chat_view: Вигляд чату
Password is invalid: Пароль недійсний.
oauth.client_identifier.invalid: Недійсний ідентифікатор клієнта OAuth!
oauth2.grant.post_comment.all: Створювати, редагувати або видаляти ваші
коментарі до дописів, а також голосувати, поширювати або скаржитися на
будь-який коментар до допису.
contact_email: Е-пошта для звʼязку
edited_post: Редагує допис
add_comment: Додати коментар
oauth2.grant.admin.user.purge: Повністю видаляти користувачів з вашого інстансу.
admin_panel: Панель адміністратора
months: Місяці
update_comment: Оновити коментар
type.video: Відео
added_new_post: Додає новий допис
all_magazines: Усі спільноти
2fa.qr_code_link.title: Перейшовши за цим посиланням, ви дозволите вашій
платформі зареєструвати цю двофакторну автентифікацію
enter_your_comment: Введіть ваш коментар
infinite_scroll_help: Автоматично завантажувати більше вмісту, коли ви досягнете
низу сторінки.
reject: Відмовити
2fa.backup-create.help: Ви можете створити нові резервні коди автентифікації; це
зробить наявні коди недійсними.
expires: Спливає
articles: Гілки
favourite: Уподоба
oauth2.grant.user.notification.delete: Очищати ваші сповіщення.
two_factor_authentication: Двофакторна автентифікація
replied_to_your_comment: Відповідає на ваш коментар
show_avatars_on_comments_help: Показати/приховати аватари користувачів під час
перегляду коментарів до окремої гілки чи допису.
2fa.verify_authentication_code.label: Введіть код двофакторної автентифікації,
щоб підтвердити налаштування
oauth2.grant.post.all: Створювати, редагувати або видаляти ваші мікроблоги, а
також голосувати, поширювати або скаржитися на будь-який мікроблог.
show_users_avatars: Показувати аватари користувачів
Your account has been banned: Ваш обліковий запис заборонено.
oauth.consent.app_has_permissions: вже може виконувати наступні дії
eng: АНГЛ
is_adult: 18+ / делікатне
email.delete.title: Запит на видалення облікового запису
magazine: Спільнота
kbin_promo_desc: '%link_start%Клонуйте сховище%link_end% і розбудовуйте Федіверс'
share: Поділитися
purge: Очистити
add: Додати
agree_terms: Погоджуюся з %terms_link_start%Правилами та умовами%terms_link_end%
і %policy_link_start%Політикою приватності%policy_link_end%
removed_post_by: видаляє допис, створений
oauth2.grant.block.general: Блокувати або розблокувати будь-яку спільноту, домен
або користувача, а також переглядати спільноти, домени та користувачів, яких
ви заблокували.
privacy: Приватність
repeat_password: Повторіть пароль
created: Створено
moderation.report.reject_report_confirmation: Ви впевнені, що хочете відхилити
цю скаргу?
old_email: Поточна е-пошта
oauth2.grant.post_comment.create: Створювати нові коментарі до дописів.
oauth2.grant.user.block: Блокувати або розблокувати користувачів, а також читати
список користувачів, яких ви блокуєте.
oauth2.grant.post.delete: Видаляти ваші наявні дописи.
flash_register_success: Ласкаво просимо! Ваш обліковий запис зареєстровано.
Залишився один крок — перевірте свою поштову скриньку на наявність посилання
для активації, яке оживить ваш обліковий запис.
he_banned: забороняє
posts: Дописи
oauth2.grant.subscribe.general: Підписуватись або відстежувати будь-яку
спільноту, домен або користувача, а також переглядати спільноти, домени та
користувачів, на яких ви підписані.
oauth2.grant.moderate.post_comment.trash: Видаляти або відновлювати коментарі до
дописів у ваших модерованих спільнотах.
oauth2.grant.moderate.magazine_admin.moderators: Додавати або видаляти
модераторів у будь-якій вашій спільноті.
flash_magazine_edit_success: Спільноту успішно відредаговано.
email_confirm_link_help: Крім того, ви можете скопіювати та вставити наступне у
свій браузер
oauth2.grant.admin.entry_comment.purge: Повністю видаляти будь-який коментар у
гілці з вашого інстансу.
related_posts: Схожі дописи
show_magazines_icons: Показувати значки спільнот
boosts: Поширення
approve: Схвалити
type.smart_contract: Розумний контракт
toolbar.strikethrough: Закреслений
note: Примітка
comment_reply_position: Розміщення форми для відповіді
change_magazine: Змінити спільноту
flash_email_failed_to_sent: Лист не було відправлено.
oauth2.grant.post.create: Створювати нові дописи.
toolbar.image: Зображення
homepage: Головна
about_instance: Про інстанс
avatar: Аватар
oauth2.grant.domain.block: Блокувати або розблокувати домени та переглядати
домени, які ви заблокували.
comments: Коментарі
oauth2.grant.admin.instance.all: Переглядати й оновлювати налаштування інстансу
або інформацію про нього.
badges: Значки
kbin_promo_title: Створіть свій власний інстанс
unsubscribe: Відписатися
flash_user_edit_profile_error: Не вдалося зберегти налаштування профілю.
follow: Відстежувати
show_all: Показати все
pin: Закріпити
profile: Профіль
new_email_repeat: Підтвердити нову е-пошту
sticky_navbar_help: Панель навігації прилипне до верху сторінки, коли ви
прокрутите вниз.
toolbar.italic: Курсив
more_from_domain: Більше з домену
hide_adult: Приховати делікатний вміст
oauth2.grant.user.profile.edit: Редагувати ваш профіль.
approved: Схвалено
check_email: Перевірте вашу е-пошту
done: Готово
tokyo_night: Tokyo Night
position_top: Зверху
oauth2.grant.admin.post_comment.purge: Повністю видаляти будь-який коментар до
допису з вашого інстансу.
header_logo: Логотип заголовка
3h: 3 години
magazine_panel: Панель спільноти
oauth2.grant.moderate.magazine_admin.badges: Створювати або видаляти значки з
ваших спільнот.
menu: Меню
default_theme: Тема за замовчуванням
toolbar.emoji: Емодзі
account_deletion_button: Видалити обліковий запис
show: Показати
hide: Приховати
and: та
bookmarks: Закладки
bookmark_list_edit: Редагувати
version: Версія
hidden: Приховано
bookmark_list_create: Створити
count: Кількість
cancel_request: Скасувати запит
notification_title_new_comment: Новий коментар
comment_not_found: Коментар не знайдено
sort_by: Сортувати за
filter_by_subscription: Фільтрувати по підпискам
marked_for_deletion: Позначено для видалення
remove_media: Видалити медіа
remove_user_avatar: Видалити аватар
subscribe_for_updates: Підпишіться, щоб отримувати оновлення.
from: з
disabled: Вимкнено
enabled: Увімкнено
mark_as_adult: Позначити як NSFW
unmark_as_adult: Зняти позначку NSFW
your_account_is_not_yet_approved: Ваш обліковий запис ще не затверджено. Ми
надішлемо вам електронного листа, щойно адміністратори опрацюють вашу заявку
на реєстрацію.
account_deletion_title: Видалення облікового запису
account_deletion_immediate: Видалити негайно
oauth2.grant.user.bookmark.remove: Видалити закладки
oauth2.grant.user.bookmark_list.edit: Відредагувати ваш список закладок
oauth2.grant.user.bookmark_list.delete: Видалити ваш список закладок
================================================
FILE: translations/messages.zh_Hans.yaml
================================================
sidebar_position: 侧边栏位置
left: 左侧
right: 右侧
federation: 联邦
status: 状态
on: 开启
off: 关闭
instances: 实例
upload_file: 上传文件
from_url: 来自网址
magazine_panel: 杂志面板
reject: 拒绝
approve: 批准
ban: 封禁
unban: 解禁
ban_hashtag_btn: 封禁标签
ban_hashtag_description: 封禁标签将阻止使用此标签创建帖子,并隐藏现有的带有此标签的帖子。
unban_hashtag_btn: 解禁标签
unban_hashtag_description: 解禁标签将允许再次创建带有此标签的帖子。现有的带有此标签的帖子不再被隐藏。
filters: 过滤器
approved: 已批准
rejected: 已拒绝
add_moderator: 添加版主
add_badge: 添加徽章
bans: 封禁
created: 创建
expires: 过期
perm: 永久
expired_at: 过期于
add_ban: 添加封禁
trash: 垃圾箱
icon: 图标
done: 完成
pin: 固定
unpin: 取消固定
change_magazine: 更改杂志
change_language: 更改语言
mark_as_adult: 标记为 NSFW
unmark_as_adult: 取消标记为 NSFW
change: 更改
pinned: 已固定
preview: 预览
article: 主题
reputation: 声誉
note: 备注
writing: 写作
users: 用户
content: 内容
week: 周
weeks: 周
month: 月
months: 月
year: 年
federated: 联邦化
local: 本地
admin_panel: 管理员面板
dashboard: 仪表板
contact_email: 联系邮箱
meta: 元
instance: 实例
pages: 页面
FAQ: 常见问题
type_search_term: 输入搜索词
federation_enabled: 联邦已启用
registrations_enabled: 注册已启用
registration_disabled: 注册已禁用
restore: 恢复
add_mentions_entries: 在主题中添加提及标签
add_mentions_posts: 在帖子中添加提及标签
Password is invalid: 密码无效。
Your account is not active: 您的账号未激活。
Your account has been banned: 您的账号已被禁止。
firstname: 名字
send: 发送
active_users: 活跃用户
random_entries: 随机主题
related_entries: 相关主题
delete_account: 删除账号
purge_account: 清除账号
ban_account: 封禁账号
unban_account: 解禁账号
related_magazines: 相关杂志
random_magazines: 随机杂志
magazine_panel_tags_info: 仅在您希望根据标签将联邦内容包含在此杂志中时提供
sidebar: 侧边栏
auto_preview: 自动媒体预览
dynamic_lists: 动态列表
banned_instances: 被封禁实例
kbin_intro_title: 探索联邦宇宙
kbin_intro_desc: 是一个去中心化的内容聚合和微博平台,运行在联邦网络中。
kbin_promo_title: 创建你自己的实例
kbin_promo_desc: '%link_start%克隆仓库%link_end%并开发联邦宇宙'
captcha_enabled: 已启用验证码
header_logo: 页眉徽标
browsing_one_thread: 你现在只浏览了讨论中的一个线索!所有评论都可在帖子页面上查看。
return: 返回
boost: 转发
mercure_enabled: Mercure 已启用
tokyo_night: 东京之夜
preferred_languages: 过滤线索和帖子的语言
infinite_scroll_help: 当你滚动到页面底部时自动加载更多内容。
sticky_navbar_help: 滚动时导航栏将固定在页面顶部。
auto_preview_help: 自动展开媒体预览。
reload_to_apply: 重新加载页面以应用更改
filter.origin.label: 选择来源
filter.fields.label: 选择要搜索的字段
filter.adult.label: 选择是否显示 NSFW 内容
filter.adult.hide: 隐藏 NSFW
filter.adult.show: 显示 NSFW
filter.adult.only: 仅 NSFW
reports: 举报
federated_magazine_info: 此杂志来自一个联合服务器,可能不完整。
disconnected_magazine_info: 此杂志未收到更新(最后活动在 %days% 天前)。
always_disconnected_magazine_info: 此杂志未收到更新。
federated_user_info: 此个人资料来自一个联合服务器,可能不完整。
type.video: 视频
type.smart_contract: 智能合约
type.magazine: 杂志
thread: 主题
threads: 主题
microblog: 微博
people: 人
events: 事件
magazine: 杂志
magazines: 杂志
search: 搜索
add: 添加
select_channel: 选择频道
login: 登录
sort_by: 排序方式
active: 活跃
newest: 最新
oldest: 最旧
commented: 已评论
change_view: 更改视图
filter_by_time: 按时间过滤
filter_by_type: 按类型过滤
filter_by_subscription: 按订阅过滤
filter_by_federation: 按联合状态过滤
comments_count: '{0}评论|{1}评论|]1,Inf[ 评论'
subscribers_count: '{0}订阅者|{1}订阅者|]1,Inf[ 订阅者'
followers_count: '{0}关注者|{1}关注者|]1,Inf[ 关注者'
marked_for_deletion: 标记为删除
marked_for_deletion_at: 在 %date% 标记为删除
favourites: 收藏夹
favourite: 最爱
more: 更多
avatar: 头像
added: 已添加
up_votes: 转发
down_votes: 减少
no_comments: 没有评论
created_at: 创建于
owner: 所有者
subscribers: 订阅者
online: 在线
comments: 评论
posts: 帖子
replies: 回复
moderators: 管理员
mod_log: 管理日志
add_comment: 添加评论
add_post: 添加帖子
add_media: 添加媒体
remove_media: 移除媒体
markdown_howto: 编辑器如何工作?
enter_your_comment: 输入您的评论
enter_your_post: 输入您的帖子
related_posts: 相关帖子
random_posts: 随机帖子
subscribe_for_updates: 订阅以开始接收更新。
go_to_original_instance: 在远程实例上查看
empty: 空
subscribe: 订阅
unsubscribe: 取消订阅
follow: 关注
unfollow: 取消关注
reply: 回复
login_or_email: 登录或电子邮件
password: 密码
remember_me: 记住我
dont_have_account: 没有账号?
you_cant_login: 忘记密码?
already_have_account: 已经有账号?
register: 注册
reset_password: 重置密码
show_more: 显示更多
to: 到
in: 在
from: 来自
username: 用户名
email: 电子邮件
repeat_password: 重复密码
agree_terms: 同意 %terms_link_start%条款和条件%terms_link_end% 和
%policy_link_start%隐私政策%policy_link_end%
terms: 服务条款
privacy_policy: 隐私政策
about_instance: 关于
all_magazines: 所有杂志
stats: 统计
fediverse: 联邦宇宙
create_new_magazine: 创建新杂志
add_new_article: 添加新主题
add_new_link: 添加新链接
add_new_photo: 添加新照片
add_new_post: 添加新帖子
add_new_video: 添加新视频
contact: 联系
faq: 常见问题
rss: RSS
change_theme: 更改主题
downvotes_mode: 点踩模式
change_downvotes_mode: 更改点踩模式
disabled: 已禁用
hidden: 已隐藏
enabled: 已启用
useful: 有用
help: 帮助
check_email: 检查您的电子邮件
reset_check_email_desc: 如果您的电子邮件地址已经关联了一个账号,您应该会很快收到一封包含重置密码链接的电子邮件。该链接将在
%expire% 后过期。
reset_check_email_desc2: 如果您没有收到电子邮件,请检查您的垃圾邮件文件夹。
try_again: 再试一次
up_vote: 转发
down_vote: 减少
email_confirm_header: 你好!确认您的电子邮件地址。
email_confirm_content: 准备激活您的 Mbin 账号吗?点击下面的链接:
email_verify: 确认电子邮件地址
email_confirm_expire: 请注意,该链接将在一小时内过期。
email_confirm_title: 确认您的电子邮件地址。
select_magazine: 选择一本杂志
add_new: 添加新内容
url: 网址
title: 标题
body: 正文
tags: 标签
tag: 标签
badges: 徽章
is_adult: 18+ / NSFW
eng: 英语
oc: 原创内容
image: 图片
image_alt: 图片替代文本
name: 名称
description: 描述
rules: 规则
domain: 域名
followers: 关注者
following: 正在关注
subscriptions: 订阅
overview: 概述
cards: 卡片
columns: 列
user: 用户
joined: 加入时间
moderated: 已审核
people_local: 本地
people_federated: 联邦
reputation_points: 声望积分
related_tags: 相关标签
go_to_content: 转到内容
go_to_filters: 转到过滤器
go_to_search: 转到搜索
subscribed: 已订阅
all: 全部
logout: 登出
classic_view: 经典视图
compact_view: 紧凑视图
chat_view: 聊天视图
tree_view: 树状视图
table_view: 表格视图
3h: 3 小时
6h: 6 小时
12h: 12 小时
1d: 1 天
1w: 1 周
1m: 1 个月
1y: 1 年
links: 链接
articles: 主题
photos: 照片
videos: 视频
share: 分享
copy_url: 复制 Mbin URL
copy_url_to_fediverse: 复制原始 URL
share_on_fediverse: 在联邦网络上分享
edit: 编辑
are_you_sure: 您确定吗?
moderate: 审核
reason: 理由
edit_entry: 编辑主题
delete: 删除
edit_post: 编辑帖子
edit_comment: 保存更改
menu: 菜单
settings: 设置
general: 常规
profile: 个人资料
blocked: 已屏蔽
notifications: 通知
messages: 消息
appearance: 外观
homepage: 主页
hide_adult: 隐藏 NSFW 内容
featured_magazines: 推荐杂志
privacy: 隐私
show_profile_subscriptions: 显示杂志订阅
show_profile_followings: 显示关注的用户
notify_on_new_entry_reply: 我撰写的主题中的任何级别评论
notify_on_new_entry_comment_reply: 对我在任何主题中的评论的回复
notify_on_new_post_reply: 我撰写的帖子中的任何级别回复
notify_on_new_post_comment_reply: 对我在任何帖子中的评论的回复
notify_on_new_entry: 我订阅的任何杂志中的新主题(链接或文章)
notify_on_new_posts: 我订阅的任何杂志中的新帖子
notify_on_user_signup: 新注册用户
save: 保存
about: 关于
old_email: 当前电子邮件
new_email: 新电子邮件
new_email_repeat: 确认新电子邮件
current_password: 当前密码
new_password: 新密码
change_email: 更改电子邮件
change_password: 更改密码
expand: 展开
collapse: 折叠
domains: 域名
error: 错误
votes: 投票
theme: 主题
dark: 黑暗
light: 明亮
solarized_light: 太阳能亮色
solarized_dark: 太阳能暗色
default_theme: 默认主题
default_theme_auto: 明亮/黑暗(自动检测)
solarized_auto: 太阳能(自动检测)
font_size: 字体大小
size: 大小
boosts: 转发
show_users_avatars: 显示用户头像
yes: 是
no: 否
show_magazines_icons: 显示杂志图标
show_thumbnails: 显示缩略图
rounded_edges: 圆角边缘
removed_thread_by: 已删除主题由
restored_thread_by: 已恢复主题由
removed_comment_by: 已删除评论由
restored_comment_by: 已恢复评论由
removed_post_by: 已删除帖子由
restored_post_by: 已恢复帖子由
he_banned: 封禁
he_unbanned: 解封
read_all: 阅读全部
show_all: 显示全部
flash_register_success: 欢迎加入!您的账号现在已注册。最后一步 - 检查您的收件箱以获取激活链接,使您的账号生效。
flash_thread_new_success: 主题已成功创建,现在对其他用户可见。
flash_thread_edit_success: 主题已成功编辑。
flash_thread_delete_success: 主题已成功删除。
flash_thread_pin_success: 主题已成功置顶。
flash_thread_unpin_success: 主题已成功取消置顶。
flash_magazine_new_success: 杂志已成功创建。您现在可以添加新内容或探索杂志的管理面板。
flash_magazine_edit_success: 杂志已成功编辑。
flash_mark_as_adult_success: 帖子已成功标记为 NSFW。
flash_unmark_as_adult_success: 帖子已成功取消 NSFW 标记。
too_many_requests: 超出限制,请稍后再试。
set_magazines_bar: 杂志栏
set_magazines_bar_desc: 在逗号后添加杂志名称
set_magazines_bar_empty_desc: 如果字段为空,将在栏中显示活动杂志。
mod_log_alert: 警告 - Modlog 可能包含已被版主删除的不愉快或令人不安的内容。请谨慎处理。
added_new_thread: 添加了新主题
edited_thread: 编辑了主题
mod_remove_your_thread: 版主已删除您的主题
added_new_comment: 添加了新评论
edited_comment: 编辑了评论
replied_to_your_comment: 回复了您的评论
infinite_scroll: 无限滚动
show_top_bar: 显示顶部栏
sticky_navbar: 固定导航栏
subject_reported: 内容已被举报。
password_confirm_header: 确认你的密码更改请求。
your_account_is_not_active: 你的账号尚未激活。请检查你的电子邮件以获取账号激活说明,或请求新的账号激活邮件。
your_account_has_been_banned: 你的账号已被禁用
your_account_is_not_yet_approved: 你的账号尚未获得批准。管理员处理你的注册请求后,我们将立即发送电子邮件通知你。
toolbar.bold: 粗体
toolbar.italic: 斜体
toolbar.strikethrough: 删除线
toolbar.header: 标题
toolbar.quote: 引用
toolbar.code: 代码
toolbar.link: 链接
toolbar.image: 图片
toolbar.unordered_list: 无序列表
toolbar.ordered_list: 有序列表
toolbar.mention: 提及
toolbar.spoiler: 剧透
federation_page_enabled: 联邦页面已启用
federation_page_allowed_description: 我们联邦的已知实例
federation_page_disallowed_description: 我们不联邦的实例
federation_page_dead_title: 失效实例
federation_page_dead_description: 连续至少 10 个活动无法送达,且最后一次成功送达和接收已超过一周的实例
federated_search_only_loggedin: 未登录时联邦搜索受限
account_deletion_title: 账号删除
account_deletion_description: 除非您选择立即删除账号,否则您的账号将在 30 天后被删除。要在 30
天内恢复账号,请使用相同的用户凭据登录或联系管理员。
account_deletion_button: 删除账号
account_deletion_immediate: 立即删除
more_from_domain: 更多来自该域名
errors.server500.title: 500 内部服务器错误
errors.server403.title: 403 禁止访问
email_confirm_button_text: 确认您的密码更改请求
email_confirm_link_help: 或者您可以复制并粘贴以下链接到浏览器
email.delete.title: 用户账号删除请求
email.delete.description: 以下用户已请求删除其账号
resend_account_activation_email_question: 账号不活跃?
resend_account_activation_email: 重新发送账号激活邮件
resend_account_activation_email_error: 提交此请求时出现问题。可能没有与该邮箱关联的账号,或者账号已经激活。
resend_account_activation_email_success: 如果存在与该邮箱关联的账号,我们将发送新的激活邮件。
resend_account_activation_email_description: 输入与您账号关联的邮箱地址。我们将为您重新发送一封激活邮件。
ignore_magazines_custom_css: 忽略杂志的自定义 CSS
oauth.consent.title: OAuth2 同意表单
oauth.consent.grant_permissions: 授予权限
oauth.consent.app_requesting_permissions: 希望代表您执行以下操作
oauth.consent.app_has_permissions: 已经可以执行以下操作
oauth.consent.to_allow_access: 要允许此访问,请点击下方的"允许"按钮
oauth.consent.allow: 允许
oauth.consent.deny: 拒绝
oauth.client_identifier.invalid: 无效的 OAuth 客户端 ID!
oauth.client_not_granted_message_read_permission: 此应用未获得读取您消息的权限。
restrict_oauth_clients: 将 OAuth2 客户端创建限制为管理员
private_instance: 强制用户在访问任何内容之前登录
block: 屏蔽
unblock: 取消屏蔽
oauth2.grant.moderate.magazine.ban.delete: 在您管理的杂志中解禁用户。
oauth2.grant.moderate.magazine.list: 读取您管理的杂志列表。
oauth2.grant.moderate.magazine.reports.all: 管理您所管理的杂志中的举报。
oauth2.grant.moderate.magazine.reports.read: 读取您所管理的杂志中的举报。
oauth2.grant.moderate.magazine_admin.create: 创建新杂志。
oauth2.grant.moderate.magazine_admin.delete: 删除您拥有的任何杂志。
oauth2.grant.moderate.magazine_admin.update: 编辑您拥有的杂志的规则、描述、NSFW 状态或图标。
oauth2.grant.moderate.magazine_admin.edit_theme: 编辑您拥有的杂志的自定义 CSS。
oauth2.grant.moderate.magazine_admin.moderators: 添加或移除您拥有的杂志的版主。
oauth2.grant.moderate.magazine_admin.badges: 在您拥有的杂志中创建或移除徽章。
oauth2.grant.moderate.magazine_admin.tags: 在您拥有的杂志中创建或移除标签。
oauth2.grant.moderate.magazine_admin.stats: 查看您拥有的杂志的内容、投票和统计数据。
oauth2.grant.admin.all: 对您的实例执行任何管理操作。
oauth2.grant.admin.entry.purge: 完全删除您实例中的任何主题。
oauth2.grant.read.general: 阅读您有权访问的所有内容。
oauth2.grant.write.general: 创建或编辑您的任何主题、帖子或评论。
oauth2.grant.delete.general: 删除您的任何主题、帖子或评论。
oauth2.grant.report.general: 举报主题、帖子或评论。
oauth2.grant.vote.general: 对主题、帖子或评论进行点赞、转发或点踩。
oauth2.grant.subscribe.general: 订阅或关注任何杂志、域名或用户,并查看您订阅的杂志、域名和用户。
oauth2.grant.block.general: 屏蔽或取消屏蔽任何杂志、域名或用户,并查看您屏蔽的杂志、域名和用户。
oauth2.grant.domain.all: 订阅或屏蔽域名,并查看您订阅或屏蔽的域名。
oauth2.grant.domain.subscribe: 订阅或取消订阅域名,并查看您订阅的域名。
oauth2.grant.domain.block: 屏蔽或取消屏蔽域名,并查看您屏蔽的域名。
oauth2.grant.entry.all: 创建、编辑或删除您的主题,并对任何主题进行投票、转发或举报。
oauth2.grant.entry.create: 创建新主题。
oauth2.grant.entry.edit: 编辑您现有的主题。
oauth2.grant.entry.delete: 删除您现有的主题。
oauth2.grant.entry.vote: 对任何主题进行点赞、转发或点踩。
oauth2.grant.entry.report: 举报任何主题。
oauth2.grant.entry_comment.all: 在主题中创建、编辑或删除您的评论,并对主题中的任何评论进行投票、转发或举报。
oauth2.grant.entry_comment.create: 在主题中创建新评论。
oauth2.grant.entry_comment.edit: 编辑您在主题中现有的评论。
oauth2.grant.entry_comment.delete: 删除您在主题中现有的评论。
oauth2.grant.entry_comment.vote: 对主题中的任何评论进行点赞、转发或点踩。
oauth2.grant.entry_comment.report: 举报主题中的任何评论。
oauth2.grant.magazine.all: 订阅或屏蔽杂志,并查看您订阅或屏蔽的杂志。
oauth2.grant.magazine.subscribe: 订阅或取消订阅杂志,并查看您订阅的杂志。
oauth2.grant.magazine.block: 屏蔽或取消屏蔽杂志,并查看您屏蔽的杂志。
oauth2.grant.post.all: 创建、编辑或删除您的微博,并对任何微博进行投票、转发或举报。
oauth2.grant.post.create: 创建新帖。
oauth2.grant.post.edit: 编辑您现有的帖子。
oauth2.grant.post.delete: 删除您现有的帖子。
oauth2.grant.post.report: 举报任何帖子。
oauth2.grant.post_comment.all: 创建、编辑或删除您在帖子上的评论,并对任何评论进行投票、转发或举报。
oauth2.grant.post_comment.create: 在帖子上创建新评论。
oauth2.grant.post_comment.edit: 编辑您在帖子上的现有评论。
oauth2.grant.post_comment.delete: 删除您在帖子上的现有评论。
oauth2.grant.post_comment.vote: 对帖子中的任何评论进行点赞、转发或点踩。
oauth2.grant.post_comment.report: 举报帖子上的任何评论。
oauth2.grant.user.all:
阅读和编辑您的个人资料、消息或通知;阅读和编辑您授予其他应用的权限;关注或屏蔽其他用户;查看您关注或屏蔽的用户列表。
oauth2.grant.user.profile.all: 阅读和编辑您的个人资料。
oauth2.grant.user.profile.read: 阅读您的个人资料。
oauth2.grant.user.profile.edit: 编辑您的个人资料。
oauth2.grant.user.message.all: 阅读您的消息并向其他用户发送消息。
oauth2.grant.user.message.read: 阅读您的消息。
oauth2.grant.user.message.create: 向其他用户发送消息。
oauth2.grant.user.notification.all: 阅读和清除您的通知。
oauth2.grant.user.notification.read: 阅读您的通知,包括消息通知。
oauth2.grant.user.notification.delete: 清除您的通知。
oauth2.grant.user.oauth_clients.all: 阅读和编辑您授予其他 OAuth2 应用的权限。
oauth2.grant.user.oauth_clients.read: 阅读您授予其他 OAuth2 应用的权限。
oauth2.grant.user.oauth_clients.edit: 编辑您授予其他 OAuth2 应用的权限。
oauth2.grant.user.follow: 关注或取消关注用户,并阅读您关注的用户列表。
oauth2.grant.user.block: 屏蔽或取消屏蔽用户,并阅读您屏蔽的用户列表。
oauth2.grant.moderate.all: 在您管理的杂志中执行您有权限执行的任何管理操作。
oauth2.grant.moderate.entry.all: 管理您管理的杂志中的主题。
oauth2.grant.moderate.entry.change_language: 更改您管理的杂志中主题的语言。
oauth2.grant.moderate.entry.pin: 将主题固定在您管理的杂志顶部。
oauth2.grant.moderate.entry.set_adult: 在您管理的杂志中将主题标记为 NSFW。
oauth2.grant.moderate.entry.trash: 在您管理的杂志中删除或恢复主题。
oauth2.grant.moderate.entry_comment.all: 管理您管理的杂志中的评论。
oauth2.grant.moderate.entry_comment.change_language: 更改您管理的杂志中评论的语言。
oauth2.grant.moderate.entry_comment.set_adult: 在您管理的杂志中将评论标记为 NSFW。
oauth2.grant.moderate.entry_comment.trash: 在您管理的杂志中删除或恢复评论。
oauth2.grant.moderate.post.all: 管理您管理的杂志中的帖子。
oauth2.grant.moderate.post.change_language: 更改您管理的杂志中帖子的语言。
oauth2.grant.moderate.post.set_adult: 在您管理的杂志中将帖子标记为 NSFW。
oauth2.grant.moderate.post.trash: 在您管理的杂志中删除或恢复帖子。
oauth2.grant.moderate.post_comment.all: 在您管理的杂志中审核帖子上的评论。
oauth2.grant.moderate.post_comment.change_language: 更改您管理的杂志中帖子上的评论语言。
oauth2.grant.moderate.post_comment.set_adult: 在您管理的杂志中将帖子上的评论标记为 NSFW。
oauth2.grant.moderate.post_comment.trash: 在您管理的杂志中删除或恢复帖子上的评论。
oauth2.grant.moderate.magazine.ban.all: 管理您管理的杂志中的被封禁用户。
oauth2.grant.moderate.magazine.ban.read: 查看您管理的杂志中的被封禁用户。
oauth2.grant.moderate.magazine.ban.create: 在您管理的杂志中封禁用户。
oauth2.grant.admin.entry_comment.purge: 从您的实例中完全删除线程中的任何评论。
oauth2.grant.admin.post.purge: 从您的实例中完全删除任何帖子。
oauth2.grant.admin.post_comment.purge: 从您的实例中完全删除帖子上的任何评论。
oauth2.grant.admin.magazine.all: 在您的实例中在杂志之间移动线程或完全删除杂志。
oauth2.grant.admin.magazine.move_entry: 在您的实例中在杂志之间移动线程。
oauth2.grant.admin.magazine.purge: 在您的实例中完全删除杂志。
oauth2.grant.admin.user.all: 在您的实例中封禁、验证或完全删除用户。
oauth2.grant.admin.user.ban: 在您的实例中封禁或解禁用户。
oauth2.grant.admin.user.verify: 在您的实例中验证用户。
oauth2.grant.admin.user.delete: 从您的实例中删除用户。
oauth2.grant.admin.user.purge: 从您的实例中完全删除用户。
oauth2.grant.admin.instance.all: 查看和更新实例设置或信息。
oauth2.grant.admin.instance.stats: 查看您的实例统计。
oauth2.grant.admin.instance.settings.all: 查看或更新您的实例设置。
oauth2.grant.admin.instance.settings.read: 查看您的实例设置。
oauth2.grant.admin.instance.settings.edit: 更新您的实例设置。
oauth2.grant.admin.instance.information.edit: 更新关于、常见问题、联系、服务条款和隐私政策页面。
oauth2.grant.admin.federation.all: 查看和更新当前去联邦的实例。
oauth2.grant.admin.federation.read: 查看去联邦实例列表。
oauth2.grant.admin.federation.update: 向去联邦实例列表中添加或删除实例。
oauth2.grant.admin.oauth_clients.all: 查看或撤销您实例上存在的 OAuth2 客户端。
oauth2.grant.admin.oauth_clients.read: 查看您实例上存在的 OAuth2 客户端及其使用统计。
oauth2.grant.admin.oauth_clients.revoke: 撤销对您实例上 OAuth2 客户端的访问。
last_active: 最后活跃
flash_post_pin_success: 帖子已成功置顶。
flash_post_unpin_success: 帖子已成功取消置顶。
comment_reply_position_help: 在页面顶部或底部显示评论回复表单。启用"无限滚动"时,位置将始终显示在顶部。
show_avatars_on_comments: 显示评论头像
single_settings: 单个
update_comment: 更新评论
show_avatars_on_comments_help: 在查看单个线程或帖子的评论时显示/隐藏用户头像。
comment_reply_position: 评论回复位置
magazine_theme_appearance_custom_css: 自定义 CSS,将在浏览杂志内容时应用。
magazine_theme_appearance_icon: 杂志的自定义图标。如果未选择,将使用默认图标。
magazine_theme_appearance_background_image: 自定义背景图像,将在浏览杂志内容时应用。
moderation.report.ban_user_description: 您是否要封禁创建此内容的用户 (%username%) 访问此杂志?
subject_reported_exists: 此内容已被举报。
moderation.report.ban_user_title: 封禁用户
oauth2.grant.moderate.post.pin: 将帖子置顶到您管理的杂志顶部。
delete_content: 删除内容
purge_content: 清除内容
delete_content_desc: 删除用户的内容,同时保留其他用户在创建的主题、帖子和评论中的回复。
purge_content_desc: 完全清除用户的内容,包括删除其他用户在创建的主题、帖子和评论中的回复。
delete_account_desc: 删除账号,包括其他用户在创建的主题、帖子和评论中的回复。
schedule_delete_account: 定时删除
schedule_delete_account_desc: 安排在 30 天内删除此账号。这将隐藏用户及其内容,并阻止用户登录。
remove_schedule_delete_account: 取消定时删除
remove_schedule_delete_account_desc: 取消定时删除。所有内容将重新可用,用户将能够登录。
two_factor_authentication: 双重认证
two_factor_backup: 双重认证备份码
2fa.authentication_code.label: 认证码
2fa.verify: 验证
2fa.code_invalid: 认证码无效
2fa.setup_error: 为账号启用双重认证时出错
2fa.enable: 设置双重认证
2fa.disable: 禁用双重认证
2fa.backup: 您的双重认证备份码
2fa.backup-create.help: 您可以创建新的备份认证码;这样做将使现有的码无效。
2fa.backup-create.label: 创建新的备份认证码
2fa.remove: 移除双重认证
2fa.add: 添加到我的账号
2fa.verify_authentication_code.label: 输入双重认证码以验证设置
2fa.qr_code_img.alt: 允许为您的账号设置双重认证的二维码
2fa.qr_code_link.title: 访问此链接可能允许您的平台注册此双重认证
2fa.user_active_tfa.title: 用户已启用双重认证
2fa.available_apps: 使用双重认证应用程序,如 %google_authenticator%、%aegis%(Android)或
%raivo%(iOS)扫描二维码。
2fa.backup_codes.help:
当您没有双重认证设备或应用程序时,可以使用这些码。这些码将不会再次显示 ,并且每个码仅可使用一次 。
2fa.backup_codes.recommendation: 建议您将它们的副本保存在安全的地方。
cancel: 取消
password_and_2fa: 密码和双重认证
flash_account_settings_changed: 您的账号设置已成功更改。您需要重新登录。
show_subscriptions: 显示订阅
subscription_sort: 排序
alphabetically: 按字母顺序
subscriptions_in_own_sidebar: 在单独的侧边栏中
sidebars_same_side: 侧边栏在同一侧
subscription_sidebar_pop_out_right: 移动到右侧单独的侧边栏
subscription_sidebar_pop_out_left: 移动到左侧单独的侧边栏
subscription_sidebar_pop_in: 将订阅移动到内联面板
subscription_panel_large: 大面板
subscription_header: 已订阅的杂志
close: 关闭
position_bottom: 底部
position_top: 顶部
pending: 待处理
flash_thread_new_error: 无法创建主题。出现了一些问题。
flash_thread_tag_banned_error: 无法创建主题。内容不被允许。
flash_image_download_too_large_error: 无法创建图像,图像过大(最大尺寸 %bytes%)
flash_email_was_sent: 邮件已成功发送。
flash_email_failed_to_sent: 邮件无法发送。
flash_post_new_success: 帖子已成功创建。
flash_post_new_error: 无法创建帖子。出现了一些问题。
flash_magazine_theme_changed_success: 已成功更新杂志外观。
flash_magazine_theme_changed_error: 更新杂志外观失败。
flash_comment_new_success: 评论已成功创建。
flash_comment_edit_success: 评论已成功更新。
flash_comment_new_error: 无法创建评论。出现了一些问题。
flash_comment_edit_error: 无法编辑评论。出现了一些问题。
flash_user_settings_general_success: 用户设置已成功保存。
flash_user_settings_general_error: 保存用户设置失败。
flash_user_edit_profile_error: 保存个人资料设置失败。
flash_user_edit_profile_success: 用户个人资料设置已成功保存。
flash_user_edit_email_error: 更改邮箱失败。
flash_user_edit_password_error: 更改密码失败。
flash_thread_edit_error: 编辑主题失败。出现了一些问题。
flash_post_edit_error: 编辑帖子失败。出现了一些问题。
flash_post_edit_success: 帖子已成功编辑。
page_width: 页面宽度
page_width_max: 最大
page_width_auto: 自动
page_width_fixed: 固定
filter_labels: 筛选标签
auto: 自动
open_url_to_fediverse: 打开原始网址
change_my_avatar: 更改我的头像
change_my_cover: 更改我的封面
edit_my_profile: 编辑我的个人资料
account_settings_changed: 您的账号设置已成功更改。您需要重新登录。
magazine_deletion: 杂志删除
delete_magazine: 删除杂志
restore_magazine: 恢复杂志
purge_magazine: 清除杂志
magazine_is_deleted: 杂志已删除。您可以在 30 天内恢复 。
suspend_account: 暂停账号
unsuspend_account: 解除账号暂停
account_suspended: 账号已被暂停。
account_unsuspended: 账号已解除暂停。
deletion: 删除
user_suspend_desc: 暂停您的账号会隐藏您在实例上的内容,但不会永久删除,并且您可以随时恢复。
account_banned: 账号已被封禁。
account_unbanned: 账号已解除封禁。
remove_subscriptions: 删除订阅
apply_for_moderator: 申请成为版主
request_magazine_ownership: 请求杂志所有权
cancel_request: 取消请求
abandoned: 已放弃
ownership_requests: 所有权请求
accept: 接受
moderator_requests: 版主请求
action: 操作
user_badge_op: 楼主
user_badge_admin: 管理员
user_badge_global_moderator: 全局版主
user_badge_moderator: 版主
user_badge_bot: 机器人
announcement: 公告
keywords: 关键词
deleted_by_moderator: 线程、帖子或评论已被版主删除
deleted_by_author: 线程、帖子或评论已被作者删除
sensitive_warning: 敏感内容
sensitive_toggle: 切换敏感内容的可见性
sensitive_show: 点击显示
sensitive_hide: 点击隐藏
details: 详情
spoiler: 剧透
all_time: 所有时间
show: 显示
hide: 隐藏
edited: 已编辑
sso_registrations_enabled: 已启用 SSO 注册
sso_registrations_enabled.error: 当前禁用与第三方身份管理器的新账号注册。
sso_only_mode: 仅限制登录和注册为 SSO 方法
related_entry: 相关
restrict_magazine_creation: 仅限管理员和全球版主创建本地杂志
sso_show_first: 在登录和注册页面上优先显示 SSO
continue_with: 继续使用
reported_user: 被举报用户
reporting_user: 举报用户
reported: 已举报
report_subject: 主题
own_report_rejected: 您的举报已被拒绝
own_report_accepted: 您的举报已被接受
own_content_reported_accepted: 您的内容的举报已被接受。
report_accepted: 举报已被接受
open_report: 打开举报
back: 返回
magazine_log_mod_added: 已添加一名版主
magazine_log_mod_removed: 已移除一名版主
magazine_log_entry_pinned: 固定条目
magazine_log_entry_unpinned: 已移除固定条目
last_updated: 最后更新
and: 和
direct_message: 直接消息
manually_approves_followers: 手动批准关注者
register_push_notifications_button: 注册推送通知
unregister_push_notifications_button: 移除推送注册
test_push_notifications_button: 测试推送通知
test_push_message: 你好,世界!
notification_title_removed_comment: 评论已被移除
notification_title_edited_comment: 评论已被编辑
notification_title_mention: 您被提及
notification_title_new_reply: 新回复
notification_title_new_thread: 新线程
notification_title_removed_thread: 线程已被移除
notification_title_edited_thread: 线程已被编辑
notification_title_ban: 您已被封禁
notification_title_message: 新直接消息
notification_title_removed_post: 帖子已被移除
notification_title_edited_post: 帖子已被编辑
notification_title_new_signup: 新用户已注册
notification_body_new_signup: 用户 %u% 已注册。
notification_body2_new_signup_approval: 您需要在他们登录之前批准请求
show_related_magazines: 显示随机杂志
show_related_entries: 显示随机线程
show_related_posts: 显示随机帖子
show_active_users: 显示活跃用户
notification_title_new_report: 已创建新举报
magazine_posting_restricted_to_mods_warning: 只有版主可以在此杂志中创建线程
flash_posting_restricted_error: 在此杂志中创建线程仅限版主,而您不是版主
server_software: 服务器软件
version: 版本
last_successful_deliver: 最后成功送达
last_successful_receive: 最后成功接收
last_failed_contact: 最后失败联系
magazine_posting_restricted_to_mods: 限制线程创建为版主
new_user_description: 此用户是新用户(活跃时间少于 %days% 天)
new_magazine_description: 此杂志是新杂志(活跃时间少于 %days% 天)
admin_users_active: 活跃
admin_users_inactive: 非活跃
admin_users_suspended: 已暂停
admin_users_banned: 已封禁
user_verify: 激活账号
max_image_size: 最大文件大小
comment_not_found: 评论未找到
bookmark_add_to_list: 将书签添加到 %list%
bookmark_remove_from_list: 从 %list% 移除书签
bookmark_remove_all: 移除所有书签
bookmark_add_to_default_list: 将书签添加到默认列表
bookmark_lists: 书签列表
bookmarks: 书签
bookmarks_list: 在 %list% 中的书签
count: 计数
is_default: 是否默认
bookmark_list_create: 创建
bookmark_list_create_placeholder: 输入名称...
bookmark_list_create_label: 列表名称
bookmarks_list_edit: 编辑书签列表
bookmark_list_edit: 编辑
bookmark_list_selected_list: 选定列表
table_of_contents: 目录
search_type_all: 主题 + 微博
search_type_entry: 主题
search_type_post: 微博
select_user: 选择用户
new_users_need_approval: 新用户必须经过管理员批准才能登录。
signup_requests: 注册请求
application_text: 申请文本
signup_requests_header: 注册请求
signup_requests_paragraph: 这些用户希望加入您的服务器。在您批准他们的注册请求之前,他们无法登录。
flash_application_info: 管理员需要批准您的账号才能登录。您的注册请求处理完毕后,您将收到一封电子邮件。
email_application_approved_title: 您的注册请求已被批准
email_application_approved_body: 您的注册请求已被服务器管理员批准。您现在可以在 %siteName% 登录服务器。
email_application_rejected_title: 您的注册请求已被拒绝
email_application_rejected_body: 感谢您的关注,但我们遗憾地通知您,您的注册请求已被拒绝。
email_application_pending: 您的账号需要管理员批准才能登录。
email_verification_pending: 您必须验证您的电子邮件地址才能登录。
hot: 热门
top: 最佳
bot_body_content: "欢迎使用 Mbin 代理!此代理在 Mbin 中启用 ActivityPub 功能方面起着至关重要的作用。它确保 Mbin 可以与联邦宇宙中的其他实例通信和联邦。\n\
\ \nActivityPub 是一个开放标准协议,允许去中心化的社交网络平台相互通信和交互。它使不同实例(服务器)上的用户能够关注、交互和分享跨联邦社交网络(称为联邦宇宙)的内容。它为用户提供了一种标准化的方式来发布内容、关注其他用户,并进行社交互动,如对线索或帖子进行点赞、分享和评论。"
moderation.report.reject_report_confirmation: 您确定要拒绝此举报吗?
report: 举报
report_issue: 举报问题
oauth2.grant.moderate.magazine.all: 管理封禁、举报并查看您管理的杂志中的已删除项目。
moderation.report.approve_report_title: 批准举报
moderation.report.reject_report_title: 拒绝举报
moderation.report.approve_report_confirmation: 您确定要批准此举报吗?
type.link: 链接
type.article: 主题
type.photo: 照片
activity: 活动
cover: 封面
cards_view: 卡片视图
new_password_repeat: 确认新密码
mod_deleted_your_comment: 版主已删除您的评论
added_new_post: 添加了新帖子
edited_post: 编辑了帖子
mod_remove_your_post: 版主已删除您的帖子
added_new_reply: 添加了新回复
wrote_message: 写了一条消息
banned: 已封禁您
removed: 已被版主删除
deleted: 已被作者删除
mentioned_you: 提到您
comment: 评论
post: 帖子
ban_expired: 封禁已过期
purge: 清除
send_message: 发送直接消息
message: 消息
local_and_federated: 本地和联邦
filter.fields.only_names: 仅名称
filter.fields.names_and_descriptions: 名称和描述
kbin_bot: Mbin 代理
errors.server429.title: 429 请求过多
errors.server404.title: 404 未找到
custom_css: 自定义 CSS
errors.server500.description:
抱歉,我们这边出现了一些问题。如果您持续看到此错误,请尝试联系实例所有者。如果此实例完全无法工作,您可以在问题解决期间查看 %link_start%其他
Mbin 实例%link_end%。
oauth2.grant.moderate.magazine.trash.read: 查看您管理的杂志中的已删除内容。
oauth2.grant.moderate.magazine_admin.all: 创建、编辑或删除您拥有的杂志。
oauth2.grant.moderate.magazine.reports.action: 接受或拒绝您管理的杂志中的举报。
oauth2.grant.post.vote: 对任何帖子进行点赞、转发或点踩。
account_is_suspended: 用户账号已被暂停。
remove_following: 取消关注
cake_day: 蛋糕日
someone: 某人
notification_title_new_comment: 新评论
notification_title_new_post: 新帖子
bookmark_list_is_default: 是否默认列表
bookmark_list_make_default: 设为默认
================================================
FILE: translations/messages.zh_TW.yaml
================================================
type.photo: 圖片
type.video: 影片
type.smart_contract: 智能契約
type.magazine: 刊版
type.article: 帖子
thread: 帖子
threads: 帖子
microblog: 微鋪
people: 人
events: 事件
magazines: 刊版
search: 搜尋
add: 發表
select_channel: 選擇頻道
login: 登入
top: 最佳
hot: 熱門
active: 活躍
newest: 最新
oldest: 最舊
commented: 有帖子
change_view: 改變版面
filter_by_type: 按類型篩選
comments_count: '{0}評論|{1}評論|]1,Inf[ 評論'
favourites: 喜歡的
favourite: 喜歡
more: 更多
avatar: 頭像
added: 已發表
up_votes: 推
down_votes: 討厭
no_comments: 沒有留言
type.link: 連結
magazine: 刊版
filter_by_time: 按時間篩選
online: 在線
comments: 留言
posts: 鋪文
replies: 回覆
moderators: 管理員
mod_log: 管理誌
add_comment: 發表留言
add_post: 發表鋪文
add_media: 加入媒體
markdown_howto: 如何使用編輯器?
enter_your_post: 請撰寫鋪文
enter_your_comment: 請撰寫留言
activity: 活動
related_posts: 相關鋪文
random_posts: 隨緣看鋪
federated_magazine_info: 該刊版來自其他聯邦的伺服器,未必完整。
federated_user_info: 此用戶來自其他聯邦的伺服器,未必完整。
go_to_original_instance: 到原來的實體去看更多內容。
empty: 空
subscribe: 訂閲
unsubscribe: 取消訂閱
follow: 追蹤
unfollow: 取消追蹤
reply: 回覆
login_or_email: 用戶名稱或電子郵件地址
password: 密碼
remember_me: 記住我
dont_have_account: 沒有帳號嗎?
you_cant_login: 忘記密碼嗎?
already_have_account: 已有帳號了嗎?
register: 註冊
reset_password: 重設密碼
show_more: 顯示更多
username: 用戶名稱
email: 電子郵件地址
repeat_password: 重複輸入密碼
terms: 服務條款
privacy_policy: 隱私政策
about_instance: 關於
all_magazines: 所有刊版
stats: 統計
fediverse: 聯邦宇宙
create_new_magazine: 立刊
add_new_post: 新鋪文
add_new_article: 新帖子
add_new_photo: 新圖片
add_new_link: 新連結
contact: 聯繫
faq: 常見問題
rss: RSS
useful: 常用
help: 說明
add_new_video: 新影片
check_email: 請查看您的電子郵件
reset_check_email_desc2: 若您沒有收到電子郵件,請查看是否被歸類為垃圾郵件了。
try_again: 再試一次
up_vote: 推
down_vote: 討厭
email_confirm_header: 您好!請確認你的電子郵件地址。
email_confirm_title: 確認您的電子郵件地址。
select_magazine: 請選擇刊版
add_new: 新增
url: 網址
title: 標題
tags: 標籤
badges: 勳章
is_adult: 成人內容
eng: 英文
oc: 原創
image: 圖片
name: 名稱
rules: 規則
domain: 網域
followers: 追蹤者
following: 追蹤中
subscriptions: 訂閲項目
user: 用戶
joined: 加入於
people_federated: 聯邦的
subscribed: 已訂閲
all: 全部
logout: 登出
3h: 3小時
6h: 6小時
12h: 12小時
1d: 1天
1w: 1星期
1y: 1年
links: 連結
photos: 圖片
videos: 影片
report: 回報
edit: 編輯
moderate: 管理
reason: 理由
edit_post: 編輯鋪文
edit_comment: 編輯留言
settings: 設定
general: 整體
profile: 用戶檔
blocked: 封鎖項目
notifications: 通知
messages: 訊息
appearance: 外觀
hide_adult: 隱藏成人內容
featured_magazines: 推薦的刊版
privacy: 隱私
show_profile_followings: 顯示正在追蹤的用戶
notify_on_new_post_reply: 有人回覆我的鋪文時
save: 儲存
about: 關於
old_email: 目前的電子郵件地址
current_password: 目前的密碼
new_password: 新密碼
new_password_repeat: 確認新密碼
change_email: 更改電子郵件地址
change_password: 更改密碼
expand: 展開
collapse: 收起
domains: 網域
error: 錯誤
votes: 票數
dark: 黑暗
light: 明亮
font_size: 字型大小
size: 大
body: 內文
description: 描述
1m: 1個月
share: 分享
delete: 刪除
new_email: 新的電子郵件地址
new_email_repeat: 確認新的電子郵件地址
theme: 主題
moderated: 管理的刊數
reports: 回報
show_profile_subscriptions: 顯示已訂閱的刊版
created_at: 創於
owner: 所有者
subscribers: 訂閲者
change_theme: 更換主題
cards: 卡片
columns: 列
reputation_points: 聲望值
related_tags: 相關標籤
go_to_content: 進至內容
go_to_filters: 到篩選器
go_to_search: 進至搜尋
chat_view: 對話版面
tree_view: 樹狀版面
table_view: 表格版面
cards_view: 卡片版面
copy_url: 複製 Mbin 網址
copy_url_to_fediverse: 複製聯邦網址
share_on_fediverse: 分享至聯邦宇宙
are_you_sure: 您確定嗎?
articles: 帖子
notify_on_new_entry_reply: 有人回覆我的帖子時
notify_on_new_entry_comment_reply: 有人在帖子回覆我的留言時
notify_on_new_entry: 訂閱的刊版有新帖子時
removed_thread_by: 移除了此用戶的帖子:
restored_thread_by: 還原了此用戶的帖子:
removed_comment_by: 移除了此用戶的留言:
restored_comment_by: 還原了此用戶的留言:
removed_post_by: 移除了此用戶的鋪文:
restored_post_by: 還原了此用戶的鋪文:
he_banned: 封鎖
he_unbanned: 解除封鎖
read_all: 全標示為已讀
show_all: 顯示全部
flash_register_success: 歡迎!現在您的帳號只差一步就可以使用,請您的電子郵件中確認可以啟動帳號的啟用連結。
flash_thread_new_success: 成功立帖。其他用戶現在已可閲覽。
flash_thread_edit_success: 成功編輯帖子。
flash_thread_delete_success: 成功刪除帖子。
flash_thread_pin_success: 成功釘選帖子。
flash_thread_unpin_success: 成功取消釘選帖子。
comment: 留言
change_language: 更改語言
change: 更改
article: 帖子
users: 用戶
week: 週
weeks: 週
month: 月
months: 月
year: 年
federated: 聯邦的
local: 本地
overview: 概覽
people_local: 本地
compact_view: 緊湊版面
content: 內容
classic_view: 傳統版面
cover: 封面
to: 傳給
in: 在
agree_terms:
同意%terms_link_start%使用條款%terms_link_end%和%policy_link_start%隱私政策%policy_link_end%
email_verify: 確認電子郵件地址
email_confirm_expire: 請注意,此連結於一小時後失效。
image_alt: 替代文字
email_confirm_content: 準備好啟用你的 Mbin 帳號了嗎?點擊下方的連結:
homepage: 首頁
notify_on_new_post_comment_reply: 有人在任何鋪文回覆我的留言時
notify_on_new_posts: 訂閱的刊版有新鋪文時
boosts: 推
show_users_avatars: 顯示用戶頭像
yes: 是
no: 否
show_magazines_icons: 顯示刊版頭像
show_thumbnails: 顯示縮圖
rounded_edges: 圓角介面
reset_check_email_desc: 若您的電子郵件地址有連繫的帳號,您很快會收到一封含重設密碼連結的電子郵件,此連結於%expire%後失效。
icon: 頭像
message: 訊息
infinite_scroll: 無限瀏覽
subject_reported: 內容己回報。
sidebar_position: 側欄位置
left: 左側
right: 右側
status: 狀態
on: 開
off: 關
upload_file: 上傳檔案
from_url: 從網址
ban: 封鎖
filters: 篩選器
perm: 永久
expired_at: 已到期於
trash: 垃圾
FAQ: 常見問題
random_entries: 隨緣看帖
related_entries: 相關帖子
delete_account: 刪除帳號
ban_account: 封鎖帳號
unban_account: 解除封鎖帳號
related_magazines: 相關刊版
random_magazines: 隨緣看刊
preview: 預覽
reputation: 聲望
federation: 聯邦
add_moderator: 新增管理員
report_issue: 回報問題
flash_magazine_edit_success: 成功編輯刊版。
flash_magazine_new_success: 成功立刊。 你現在可以新增內容或瀏覽刊版管理面板。
set_magazines_bar_empty_desc: 如果留空,活躍的刊版便會顯示於上列。
too_many_requests: 請求己達上限,請稍候再試。
set_magazines_bar_desc: 逗號後加上刊版名稱
solarized_light: 間明色
solarized_dark: 間暗色
added_new_thread: 開了新帖子
magazine_panel: 刊版面板
deleted: 被作者刪除
mentioned_you: 提及您
post: 鋪文
ban_expired: 封鎖過期
show_top_bar: 顯示頂列
sticky_navbar: 固定導航列
added_new_post: 開了新鋪文
added_new_reply: 開了新回覆
approve: 認可
mod_remove_your_thread: 管理員移除了您的帖子
mod_remove_your_post: 管理員移除了您的鋪文
approved: 已認可
removed: 被管理員移除
reject: 駁回
rejected: 已駁回
replied_to_your_comment: 回覆了您的留言
mod_deleted_your_comment: 管理員刪除了您的留言
edited_post: 編輯了一個鋪文
wrote_message: 傳了訊息
banned: 封鎖了您
instances: 實體
edited_thread: 編輯了一個帖子
added_new_comment: 己發表留言
send_message: 傳送訊息
dynamic_lists: 動態列表
auto_preview: 自動預覽媒體
sidebar: 側欄
writing: 寫文時
Password is invalid: 密碼有誤。
admin_panel: 管理員面板
active_users: 活躍用戶
Your account has been banned: 您的帳號已被封鎖。
firstname: 名
send: 傳送
contact_email: 聯絡用電子郵件
meta: Meta
instance: 實體
unpin: 取消訂選
registrations_enabled: 已開放註冊
federation_enabled: 已連邦
pinned: 已釘選
pin: 釘選
dashboard: 資訊面板
registration_disabled: 註冊已停用
restore: 還原
Your account is not active: 您的帳號尚未啟用。
type_search_term: 輸入關鍵字
boost: 推
captcha_enabled: 已啟用Captcha
kbin_promo_title: 創造你自己的實體
kbin_intro_title: 探索聯邦宇宙
banned_instances: 封鎖中的實體
browsing_one_thread: 您現在正在瀏覽帖子的其中一文!所有的留言請從原鋪文閱覽。
return: 返回
kbin_intro_desc: 是一個在聯邦宇宙的去中心化的內容蒐集器和微鋪(微博)平台。
kbin_promo_desc: '%link_start%複製源碼目錄%link_end%以一同開發聯邦宇宙'
mod_log_alert: 警告:管理日誌可能有不討喜和令人不安而受管理員移除的內容,請謹慎小心。
mercure_enabled: Mercure 已啟動
tokyo_night: 東京之夜
set_magazines_bar: 刊版欄
bans: 封鎖
add_badge: 新增勳章
add_ban: 新增封鎖
edited_comment: 編輯了一個留言
change_magazine: 更改刊版
pages: 頁
add_mentions_entries: 在刊版中提及
add_mentions_posts: 在鋪文中提及
header_logo: 頭版圖像
infinite_scroll_help: 在滑到頁面底部時自動載入更多的內容。
reload_to_apply: 請重載頁面以更新設定
filter.origin.label: 選擇來源
filter.adult.label: 選擇是否顯示成人內容
filter.fields.only_names: 只有名字
filter.fields.names_and_descriptions: 名字和描述
kbin_bot: Mbin 機器人
preferred_languages: 過濾帖子和鋪文的語言
magazine_panel_tags_info: 不建議使用,除非你想要來自其他聯邦宇宙的內容透過標籤(hashtags) 能被引進到這個刊版
bot_body_content: "歡迎使用 Mbin 機器人!這個機器人在讓 Mbin 啟用 ActivityPub 的功能中有很重要的作用。它能確保 /kbin
可以聯繫並並與其他聯邦宇宙的實體組成聯邦。\n\nActivityPub 是一個開放、標準的傳輸協定,能夠讓去中心化的社交網路平台可以互相構通和交流。讓用戶能在不同實體(也就是伺服器)在聯邦的社交網路中-又稱為聯邦宇宙-去追蹤、交流、並分享內容。
提供一個標準,讓用戶可以推出新內容、追蹤其他用戶、進行像是按讚、分享、在刊版或鋪文留言等等的社交活動。"
auto_preview_help: 自動展開媒體預覽。
sticky_navbar_help: 在滾動頁面時,導航欄會持續貼在螢幕。
filter.adult.hide: 隱藏成人內容
filter.adult.show: 顯示成人內容
filter.adult.only: 只顯示成人內容
local_and_federated: 本地和其他聯邦宇宙的
created: 創於
done: 完成
expires: 到期於
note: 備註
purge: 清除
purge_account: 停用帳號
password_confirm_header: 請確認您的密碼變更請求。
filter.fields.label: 選擇搜尋哪個項目
sort_by: 排序方式
filter_by_subscription: 依訂閱篩選
filter_by_federation: 依聯邦狀態篩選
subscribers_count: '{0}位訂閱者|{1}位訂閱者|]1,Inf[ 位訂閱者'
followers_count: '{0}位追蹤者|{1}位追蹤者|]1,Inf[ 位追蹤者'
marked_for_deletion: 標記為待刪除
marked_for_deletion_at: 標記為待刪除於 %date%
remove_media: 移除媒體
remove_user_avatar: 移除頭像
remove_user_cover: 移除封面
disconnected_magazine_info: 此雜誌未接收更新(上次活動於 %days% 天前)。
always_disconnected_magazine_info: 此雜誌未接收更新。
subscribe_for_updates: 訂閱以開始接收更新。
from: 來自
downvotes_mode: 負評模式
change_downvotes_mode: 變更負評模式
disabled: 已停用
hidden: 已隱藏
enabled: 已啟用
tag: 標籤
crosspost: 跨鋪文
edit_entry: 編輯帖子
menu: 選單
notify_on_user_signup: 新註冊通知
default_theme: 預設主題
default_theme_auto: 淺色/深色(自動偵測)
solarized_auto: Solarized(自動偵測)
flash_mark_as_adult_success: 鋪文已成功標記為 NSFW。
flash_unmark_as_adult_success: 鋪文已成功取消標記為 NSFW。
ban_expires: 封鎖到期時間
unban: 解除封鎖
ban_hashtag_btn: 封鎖主題標籤
ban_hashtag_description: 封鎖主題標籤將阻止建立帶有此標籤的鋪文,並隱藏現有帶有此標籤的鋪文。
unban_hashtag_btn: 解除封鎖主題標籤
unban_hashtag_description: 解除封鎖主題標籤將允許再次建立帶有此標籤的鋪文。現有帶有此標籤的鋪文將不再隱藏。
banner: 橫幅
mark_as_adult: 標記為 NSFW
unmark_as_adult: 取消標記為 NSFW
type_search_term_url_handle: 輸入搜尋詞、網址或使用者代號
viewing_one_signup_request: 您正在檢視 %username% 的一項註冊請求
your_account_is_not_active: 您的帳號尚未啟用。請檢查您的電子郵件以取得帳號啟用說明,或請求新的帳號啟用郵件。
your_account_has_been_banned: 您的帳號已被封鎖
your_account_is_not_yet_approved: 您的帳號尚未獲得批准。一旦管理員處理完您的註冊請求,我們將向您發送電子郵件。
toolbar.bold: 粗體
toolbar.italic: 斜體
toolbar.strikethrough: 刪除線
toolbar.header: 標題
toolbar.quote: 引用
toolbar.code: 程式碼
toolbar.link: 連結
toolbar.image: 圖片
toolbar.unordered_list: 無序清單
toolbar.ordered_list: 有序清單
toolbar.mention: 提及
toolbar.spoiler: 劇透
toolbar.emoji: 表情符號
federation_page_enabled: 聯邦頁面已啟用
federation_page_allowed_description: 我們與之聯邦的已知實例
federation_page_disallowed_description: 我們不與之聯邦的實例
federation_page_dead_title: 失效實例
federation_page_dead_description: 我們連續無法傳送至少 10 個活動,且上次成功傳送與接收均超過一週前的實例
federated_search_only_loggedin: 未登入時聯邦搜尋受限
account_deletion_title: 帳號刪除
account_deletion_description: 您的帳號將在 30 天後刪除,除非您選擇立即刪除帳號。若要在 30
天內恢復帳號,請使用相同的使用者憑證登入或聯絡管理員。
account_deletion_button: 刪除帳號
account_deletion_immediate: 立即刪除
more_from_domain: 更多來自此網域
errors.server500.title: 500 內部伺服器錯誤
errors.server500.description:
抱歉,我們這邊出了點問題。如果您持續看到此錯誤,請嘗試聯絡站台擁有者。如果此站台完全無法運作,在問題解決之前,您可以先查看 %link_start%其他
Mbin 站台%link_end%。
errors.server429.title: 429 請求過多
errors.server404.title: 404 找不到
errors.server403.title: 403 禁止存取
email_confirm_button_text: 確認您的密碼變更請求
email_confirm_link_help: 或者,您可以將以下連結複製並貼到您的瀏覽器中
email.delete.title: 使用者帳號刪除請求
email.delete.description: 以下使用者已請求刪除其帳號
resend_account_activation_email_question: 帳號未啟用?
resend_account_activation_email: 重新發送帳號啟用電子郵件
resend_account_activation_email_error: 提交此請求時發生問題。可能沒有與該電子郵件關聯的帳號,或者該帳號可能已經啟用。
resend_account_activation_email_success: 如果存在與該電子郵件關聯的帳號,我們將發送新的啟用電子郵件。
resend_account_activation_email_description: 輸入與您帳號關聯的電子郵件地址。我們將為您重新發送一封啟用電子郵件。
custom_css: 自訂 CSS
ignore_magazines_custom_css: 忽略雜誌的自訂 CSS
oauth.consent.title: OAuth2 授權同意表單
oauth.consent.grant_permissions: 授予權限
oauth.consent.app_requesting_permissions: 希望代表您執行以下操作
oauth.consent.app_has_permissions: 已可執行以下操作
oauth.consent.to_allow_access: 若要允許此存取,請點擊下方的「允許」按鈕
oauth.consent.allow: 允許
oauth.consent.deny: 拒絕
oauth.client_identifier.invalid: 無效的 OAuth 客戶端 ID!
oauth.client_not_granted_message_read_permission: 此應用程式未獲得讀取您訊息的權限。
restrict_oauth_clients: 將 OAuth2 客戶端建立限制為管理員
private_instance: 強制使用者在登入後才能存取任何內容
block: 封鎖
unblock: 解除封鎖
oauth2.grant.moderate.magazine.ban.delete: 在您管理的雜誌中解除封鎖使用者。
oauth2.grant.moderate.magazine.list: 讀取您管理的雜誌清單。
oauth2.grant.moderate.magazine.reports.all: 管理您管理的雜誌中的檢舉。
oauth2.grant.moderate.magazine.reports.read: 讀取您管理的雜誌中的檢舉。
oauth2.grant.moderate.magazine.reports.action: 在您管理的雜誌中接受或拒絕檢舉。
oauth2.grant.moderate.magazine.trash.read: 檢視您管理的雜誌中已刪除的內容。
oauth2.grant.moderate.magazine_admin.all: 建立、編輯或刪除您擁有的雜誌。
oauth2.grant.moderate.magazine_admin.create: 建立新雜誌。
oauth2.grant.moderate.magazine_admin.delete: 刪除您擁有的任何雜誌。
oauth2.grant.moderate.magazine_admin.update: 編輯您擁有的任何雜誌的規則、描述、NSFW 狀態或圖示。
oauth2.grant.moderate.magazine_admin.edit_theme: 編輯您擁有的任何雜誌的自訂 CSS。
oauth2.grant.moderate.magazine_admin.moderators: 在您擁有的任何雜誌中新增或移除版主。
oauth2.grant.moderate.magazine_admin.badges: 在您擁有的雜誌中建立或移除徽章。
oauth2.grant.moderate.magazine_admin.tags: 在您擁有的雜誌中建立或移除標籤。
oauth2.grant.moderate.magazine_admin.stats: 檢視您擁有的雜誌的內容、投票和檢視統計資料。
oauth2.grant.admin.all: 在您的站台上執行任何管理操作。
oauth2.grant.admin.entry.purge: 完全刪除您實例中的任何帖子。
oauth2.grant.read.general: 讀取您有權存取的所有內容。
oauth2.grant.write.general: 建立或編輯您的任何帖子、鋪文或評論。
oauth2.grant.delete.general: 刪除您的任何帖子、鋪文或評論。
oauth2.grant.report.general: 檢舉帖子、鋪文或評論。
oauth2.grant.vote.general: 對帖子、鋪文或評論進行贊成、反對或推廣。
oauth2.grant.subscribe.general: 訂閱或追蹤任何雜誌、網域或使用者,並查看您訂閱的雜誌、網域和使用者。
oauth2.grant.block.general: 封鎖或解除封鎖任何雜誌、網域或使用者,並查看您已封鎖的雜誌、網域和使用者。
oauth2.grant.domain.all: 訂閱或封鎖網域,並查看您訂閱或封鎖的網域。
oauth2.grant.domain.subscribe: 訂閱或取消訂閱網域,並查看您訂閱的網域。
oauth2.grant.domain.block: 封鎖或解除封鎖網域,並查看您已封鎖的網域。
oauth2.grant.entry.all: 建立、編輯或刪除您的帖子,並對任何帖子進行投票、推廣或檢舉。
oauth2.grant.entry.create: 建立新的帖子。
oauth2.grant.entry.edit: 編輯您現有的帖子。
oauth2.grant.entry.delete: 刪除您現有的帖子。
oauth2.grant.entry.vote: 對任何帖子進行贊成、推廣或反對。
oauth2.grant.entry.report: 檢舉任何帖子。
oauth2.grant.entry_comment.all: 在帖子中建立、編輯或刪除您的評論,並對帖子中的任何評論進行投票、推廣或檢舉。
oauth2.grant.entry_comment.create: 在帖子中建立新的評論。
oauth2.grant.entry_comment.edit: 編輯您在帖子中的現有評論。
oauth2.grant.entry_comment.delete: 刪除您在帖子中的現有評論。
oauth2.grant.entry_comment.vote: 對帖子中的任何評論進行贊成、推廣或反對。
oauth2.grant.entry_comment.report: 檢舉帖子中的任何評論。
oauth2.grant.magazine.all: 訂閱或封鎖雜誌,並查看您訂閱或封鎖的雜誌。
oauth2.grant.magazine.subscribe: 訂閱或取消訂閱雜誌,並查看您訂閱的雜誌。
oauth2.grant.magazine.block: 封鎖或解除封鎖雜誌,並查看您已封鎖的雜誌。
oauth2.grant.post.all: 建立、編輯或刪除您的微鋪,並對任何微鋪進行投票、推廣或檢舉。
oauth2.grant.post.create: 建立新的鋪文。
oauth2.grant.post.edit: 編輯您現有的鋪文。
oauth2.grant.post.delete: 刪除您現有的鋪文。
oauth2.grant.post.vote: 對任何鋪文進行贊成、推廣或反對。
oauth2.grant.post.report: 檢舉任何鋪文。
oauth2.grant.post_comment.all: 在鋪文上建立、編輯或刪除您的評論,並對鋪文上的任何評論進行投票、推廣或檢舉。
oauth2.grant.post_comment.create: 在鋪文上建立新的評論。
oauth2.grant.post_comment.edit: 編輯您在鋪文上的現有評論。
oauth2.grant.post_comment.delete: 刪除您在鋪文上的現有評論。
oauth2.grant.post_comment.vote: 對鋪文上的任何評論進行贊成、推廣或反對。
oauth2.grant.post_comment.report: 檢舉鋪文上的任何評論。
oauth2.grant.user.all:
讀取和編輯您的個人資料、訊息或通知;讀取和編輯您授予其他應用程式的權限;追蹤或封鎖其他使用者;查看您追蹤或封鎖的使用者清單。
oauth2.grant.user.bookmark: 新增與移除書籤
oauth2.grant.user.bookmark.add: 新增書籤
oauth2.grant.user.bookmark.remove: 移除書籤
oauth2.grant.user.bookmark_list: 讀取、編輯與刪除您的書籤清單
oauth2.grant.user.bookmark_list.read: 讀取您的書籤清單
oauth2.grant.user.bookmark_list.edit: 編輯您的書籤清單
oauth2.grant.user.bookmark_list.delete: 刪除您的書籤清單
oauth2.grant.user.profile.all: 讀取與編輯您的個人檔案。
oauth2.grant.user.profile.read: 讀取您的個人檔案。
oauth2.grant.user.profile.edit: 編輯您的個人檔案。
oauth2.grant.user.message.all: 讀取您的訊息並傳送訊息給其他使用者。
oauth2.grant.user.message.read: 讀取您的訊息。
oauth2.grant.user.message.create: 傳送訊息給其他使用者。
oauth2.grant.user.notification.all: 讀取與清除您的通知。
oauth2.grant.user.notification.read: 讀取您的通知,包含訊息通知。
oauth2.grant.user.notification.delete: 清除您的通知。
oauth2.grant.user.oauth_clients.all: 讀取與編輯您授予其他 OAuth2 應用程式的權限。
oauth2.grant.user.oauth_clients.read: 讀取您授予其他 OAuth2 應用程式的權限。
oauth2.grant.user.oauth_clients.edit: 編輯您授予其他 OAuth2 應用程式的權限。
oauth2.grant.user.follow: 追蹤或取消追蹤使用者,並讀取您追蹤的使用者清單。
oauth2.grant.user.block: 封鎖或解除封鎖使用者,並讀取您封鎖的使用者清單。
oauth2.grant.moderate.all: 在您管理的雜誌中執行您有權執行的任何審核操作。
oauth2.grant.moderate.entry.all: 在您管理的雜誌中審核帖子。
oauth2.grant.moderate.entry.change_language: 變更您管理的雜誌中帖子的語言。
oauth2.grant.moderate.entry.pin: 將帖子置頂於您管理的雜誌中。
oauth2.grant.moderate.entry.set_adult: 在您管理的雜誌中將帖子標記為 NSFW。
oauth2.grant.moderate.entry.trash: 在您管理的雜誌中將帖子移至垃圾桶或還原。
oauth2.grant.moderate.entry_comment.all: 在您管理的雜誌中審核帖子內的留言。
oauth2.grant.moderate.entry_comment.change_language: 變更您管理的雜誌中帖子內留言的語言。
oauth2.grant.moderate.entry_comment.set_adult: 在您管理的雜誌中將帖子內的留言標記為 NSFW。
oauth2.grant.moderate.entry_comment.trash: 在您管理的雜誌中將帖子內的留言移至垃圾桶或還原。
oauth2.grant.moderate.post.all: 在您管理的雜誌中審核鋪文。
oauth2.grant.moderate.post.change_language: 變更您管理的雜誌中鋪文的語言。
oauth2.grant.moderate.post.set_adult: 在您管理的雜誌中將鋪文標記為 NSFW。
oauth2.grant.moderate.post.trash: 在您管理的雜誌中將鋪文移至垃圾桶或還原。
oauth2.grant.moderate.post_comment.all: 在您管理的雜誌中審核鋪文上的留言。
oauth2.grant.moderate.post_comment.change_language: 變更您管理的雜誌中鋪文上留言的語言。
oauth2.grant.moderate.post_comment.set_adult: 在您管理的雜誌中將鋪文上的留言標記為 NSFW。
oauth2.grant.moderate.post_comment.trash: 在您管理的雜誌中將鋪文上的留言移至垃圾桶或還原。
oauth2.grant.moderate.magazine.all: 在您管理的雜誌中管理封鎖、檢舉,並檢視已移至垃圾桶的項目。
oauth2.grant.moderate.magazine.ban.all: 在您管理的雜誌中管理被封鎖的使用者。
oauth2.grant.moderate.magazine.ban.read: 檢視您管理的雜誌中被封鎖的使用者。
oauth2.grant.moderate.magazine.ban.create: 在您管理的雜誌中封鎖使用者。
oauth2.grant.admin.entry_comment.purge: 完全刪除您實例中帖子內的任何評論。
oauth2.grant.admin.post.purge: 完全刪除您實例中的任何鋪文。
oauth2.grant.admin.post_comment.purge: 完全刪除您實例中鋪文上的任何評論。
oauth2.grant.admin.magazine.all: 在您的實例中移動帖子或完全刪除雜誌。
oauth2.grant.admin.magazine.move_entry: 在您的實例中,於雜誌之間移動帖子。
oauth2.grant.admin.magazine.purge: 完全刪除您實例中的雜誌。
oauth2.grant.admin.user.all: 在您的實例中封鎖、驗證或完全刪除使用者。
oauth2.grant.admin.user.ban: 從您的實例中封鎖或解除封鎖使用者。
oauth2.grant.admin.user.verify: 驗證您實例中的使用者。
oauth2.grant.admin.user.delete: 從您的實例中刪除使用者。
oauth2.grant.admin.user.purge: 從您的實例中完全刪除使用者。
oauth2.grant.admin.instance.all: 檢視與更新實例設定或資訊。
oauth2.grant.admin.instance.stats: 檢視您實例的統計資料。
oauth2.grant.admin.instance.settings.all: 檢視或更新您實例上的設定。
oauth2.grant.admin.instance.settings.read: 檢視您實例上的設定。
oauth2.grant.admin.instance.settings.edit: 更新您實例上的設定。
oauth2.grant.admin.instance.information.edit: 更新您實例的「關於」、常見問題、聯絡方式、服務條款與隱私權政策頁面。
oauth2.grant.admin.federation.all: 檢視與更新目前已斷開聯邦的實例。
oauth2.grant.admin.federation.read: 檢視已斷開聯邦的實例清單。
oauth2.grant.admin.federation.update: 新增或移除已斷開聯邦實例清單中的實例。
oauth2.grant.admin.oauth_clients.all: 檢視或撤銷您實例上存在的 OAuth2 客戶端。
oauth2.grant.admin.oauth_clients.read: 檢視您實例上存在的 OAuth2 客戶端及其使用統計資料。
oauth2.grant.admin.oauth_clients.revoke: 撤銷您實例上 OAuth2 客戶端的存取權限。
last_active: 最後活動時間
flash_post_pin_success: 鋪文已成功置頂。
flash_post_unpin_success: 鋪文已成功取消置頂。
comment_reply_position_help: 將評論回覆表單顯示在頁面頂部或底部。當啟用「無限捲動」時,位置將始終出現在頂部。
show_avatars_on_comments: 顯示評論頭像
single_settings: 單一
update_comment: 更新評論
show_avatars_on_comments_help: 在檢視單一帖子或鋪文的評論時,顯示/隱藏使用者頭像。
comment_reply_position: 評論回覆位置
magazine_theme_appearance_custom_css: 自訂 CSS,將在檢視您雜誌內的內容時套用。
magazine_theme_appearance_icon: 雜誌的自訂圖示。
magazine_theme_appearance_banner: 雜誌的自訂橫幅。它會顯示在所有帖子上方,應為寬幅比例(5:1,或 1500px *
300px)。
magazine_theme_appearance_background_image: 自訂背景圖片,將在檢視您雜誌內的內容時套用。
moderation.report.approve_report_title: 核准檢舉
moderation.report.reject_report_title: 駁回檢舉
moderation.report.ban_user_description: 您要封鎖在此雜誌中建立此內容的使用者(%username%)嗎?
moderation.report.approve_report_confirmation: 您確定要核准此檢舉嗎?
subject_reported_exists: 此內容已被檢舉過。
moderation.report.ban_user_title: 封鎖使用者
moderation.report.reject_report_confirmation: 您確定要拒絕此檢舉嗎?
oauth2.grant.moderate.post.pin: 將鋪文置頂於您管理的雜誌。
delete_content: 刪除內容
purge_content: 清除內容
delete_content_desc: 刪除使用者的內容,但保留其他使用者在所建立的主題、鋪文和評論中的回覆。
purge_content_desc: 完全清除使用者的內容,包括刪除其他使用者在所建立的主題、鋪文和評論中的回覆。
delete_account_desc: 刪除帳號,包括其他使用者在所建立的主題、鋪文和評論中的回覆。
schedule_delete_account: 排程刪除
schedule_delete_account_desc: 將此帳號排程於 30 天後刪除。這將會隱藏該使用者及其內容,並阻止該使用者登入。
remove_schedule_delete_account: 移除排程刪除
remove_schedule_delete_account_desc: 移除已排程的刪除。所有內容將再次可用,且使用者將能夠登入。
two_factor_authentication: 雙重驗證
two_factor_backup: 雙重驗證備用碼
2fa.authentication_code.label: 驗證碼
2fa.verify: 驗證
2fa.code_invalid: 驗證碼無效
2fa.setup_error: 為帳號啟用雙重驗證時發生錯誤
2fa.enable: 設定雙重驗證
2fa.disable: 停用雙重驗證
2fa.backup: 您的雙重驗證備用碼
2fa.backup-create.help: 您可以建立新的備用驗證碼;此操作將使現有碼失效。
2fa.backup-create.label: 建立新的備用驗證碼
2fa.remove: 移除雙重驗證
2fa.add: 新增至我的帳號
2fa.verify_authentication_code.label: 輸入雙重驗證碼以確認設定
2fa.qr_code_img.alt: 一個可用於為您帳號設定雙重驗證的 QR 碼
2fa.qr_code_link.title: 造訪此連結可能允許您的平台註冊此雙重驗證
2fa.user_active_tfa.title: 使用者已啟用雙重驗證
2fa.available_apps: 使用雙重驗證應用程式(例如 %google_authenticator%、%aegis%(Android)或
%raivo%(iOS))來掃描 QR 碼。
2fa.backup_codes.help:
當您沒有雙重驗證裝置或應用程式時,可以使用這些備用碼。您將不會再次看到它們 ,且每個碼僅能使用一次 。
2fa.backup_codes.recommendation: 建議您將它們的副本保存在安全的地方。
2fa.manual_code_hint: 若無法掃描 QR 碼,請手動輸入密鑰
cancel: 取消
password_and_2fa: 密碼與雙重驗證
flash_account_settings_changed: 您的帳號設定已成功變更。您需要重新登入。
show_subscriptions: 顯示訂閱
subscription_sort: 排序
alphabetically: 依字母順序
subscriptions_in_own_sidebar: 於獨立側邊欄中
sidebars_same_side: 側邊欄位於同一側
subscription_sidebar_pop_out_right: 移至右側獨立側邊欄
subscription_sidebar_pop_out_left: 移至左側獨立側邊欄
subscription_sidebar_pop_in: 將訂閱移至內嵌面板
subscription_panel_large: 大型面板
subscription_header: 已訂閱雜誌
close: 關閉
position_bottom: 底部
position_top: 頂部
pending: 待處理
flash_thread_new_error: 無法建立帖子。發生錯誤。
flash_thread_tag_banned_error: 無法建立帖子。內容不被允許。
flash_thread_ref_image_not_found: 無法找到 'imageHash' 所引用的圖片。
flash_image_download_too_large_error: 無法建立圖片,檔案過大(最大尺寸為 %bytes%)
flash_email_was_sent: 電子郵件已成功寄出。
flash_email_failed_to_sent: 無法寄出電子郵件。
flash_post_new_success: 鋪文已成功建立。
flash_post_new_error: 無法建立鋪文。發生錯誤。
flash_magazine_theme_changed_success: 已成功更新雜誌外觀。
flash_magazine_theme_changed_error: 更新雜誌外觀失敗。
flash_comment_new_success: 留言已成功建立。
flash_comment_edit_success: 留言已成功更新。
flash_comment_new_error: 建立留言失敗。發生錯誤。
flash_comment_edit_error: 編輯留言失敗。發生錯誤。
flash_user_settings_general_success: 使用者設定已成功儲存。
flash_user_settings_general_error: 儲存使用者設定失敗。
flash_user_edit_profile_error: 儲存個人資料設定失敗。
flash_user_edit_profile_success: 使用者個人資料設定已成功儲存。
flash_user_edit_email_error: 變更電子郵件失敗。
flash_user_edit_password_error: 變更密碼失敗。
flash_thread_edit_error: 編輯帖子失敗。發生錯誤。
flash_post_edit_error: 編輯鋪文失敗。發生錯誤。
flash_post_edit_success: 鋪文已成功編輯。
page_width: 頁面寬度
page_width_max: 最大
page_width_auto: 自動
page_width_fixed: 固定
filter_labels: 篩選標籤
auto: 自動
open_url_to_fediverse: 開啟原始網址
change_my_avatar: 變更我的頭像
change_my_cover: 變更我的封面
edit_my_profile: 編輯我的個人資料
account_settings_changed: 您的帳號設定已成功變更。您需要重新登入。
magazine_deletion: 雜誌刪除
delete_magazine: 刪除雜誌
restore_magazine: 還原雜誌
purge_magazine: 清除雜誌
magazine_is_deleted: 雜誌已被刪除。您可以在 30 天內還原 它。
suspend_account: 停用帳號
unsuspend_account: 啟用帳號
account_suspended: 帳號已被停用。
account_unsuspended: 帳號已被啟用。
deletion: 刪除
user_suspend_desc: 停用您的帳號會隱藏您在站台上的內容,但不會永久移除,您可以隨時還原。
account_banned: 帳號已被封鎖。
account_unbanned: 帳號已被解除封鎖。
account_is_suspended: 使用者帳號已停用。
remove_following: 移除追蹤
remove_subscriptions: 移除訂閱
apply_for_moderator: 申請成為版主
request_magazine_ownership: 請求雜誌所有權
cancel_request: 取消請求
abandoned: 已棄置
ownership_requests: 所有權請求
accept: 接受
moderator_requests: 版主請求
action: 操作
user_badge_op: 原發文者
user_badge_admin: 管理員
user_badge_global_moderator: 全域版主
user_badge_moderator: 版主
user_badge_bot: 機器人
announcement: 公告
keywords: 關鍵字
deleted_by_moderator: 帖子、鋪文或留言已被版主刪除
deleted_by_author: 帖子、鋪文或留言已被作者刪除
sensitive_warning: 敏感內容
sensitive_toggle: 切換敏感內容可見性
sensitive_show: 點擊顯示
sensitive_hide: 點擊隱藏
details: 詳細資訊
spoiler: 劇透
all_time: 所有時間
show: 顯示
hide: 隱藏
edited: 已編輯
sso_registrations_enabled: SSO 註冊已啟用
sso_registrations_enabled.error: 目前無法使用第三方身份管理員註冊新帳號。
sso_only_mode: 僅限 SSO 方式登入與註冊
related_entry: 相關
restrict_magazine_creation: 僅限管理員與全域版主建立本地雜誌
sso_show_first: 在登入與註冊頁面優先顯示 SSO
continue_with: 繼續使用
reported_user: 被檢舉的使用者
reporting_user: 檢舉的使用者
reported: 已檢舉
report_subject: 主題
own_report_rejected: 您的檢舉已被駁回
own_report_accepted: 您的檢舉已被接受
own_content_reported_accepted: 您內容的檢舉已被接受。
report_accepted: 檢舉已被接受
open_report: 開啟檢舉
cake_day: 蛋糕日
someone: 某人
back: 返回
magazine_log_mod_added: 已新增版主
magazine_log_mod_removed: 已移除版主
magazine_log_entry_pinned: 已置頂帖子
magazine_log_entry_unpinned: 已取消置頂帖子
last_updated: 最後更新
and: 與
direct_message: 私訊
manually_approves_followers: 手動核准追蹤者
register_push_notifications_button: 註冊推播通知
unregister_push_notifications_button: 移除推播註冊
test_push_notifications_button: 測試推播通知
test_push_message: 哈囉世界!
notification_title_new_comment: 新留言
notification_title_removed_comment: 留言已被移除
notification_title_edited_comment: 留言已被編輯
notification_title_mention: 您被提及
notification_title_new_reply: 新回覆
notification_title_new_thread: 新帖子
notification_title_removed_thread: 帖子已被移除
notification_title_edited_thread: 帖子已被編輯
notification_title_ban: 您已被封鎖
notification_title_message: 新私訊
notification_title_new_post: 新鋪文
notification_title_removed_post: 鋪文已被移除
notification_title_edited_post: 鋪文已被編輯
notification_title_new_signup: 新使用者註冊
notification_body_new_signup: 使用者 %u% 已註冊。
notification_body2_new_signup_approval: 您需要在他們登入前核准此請求
show_related_magazines: 顯示隨機雜誌
show_related_entries: 顯示隨機帖子
show_related_posts: 顯示隨機鋪文
show_active_users: 顯示活躍使用者
notification_title_new_report: 已建立新檢舉
magazine_posting_restricted_to_mods_warning: 只有版主可以在這個雜誌建立帖子
flash_posting_restricted_error: 在此雜誌中建立帖子僅限版主,而您並非版主
server_software: 伺服器軟體
version: 版本
last_successful_deliver: 最後成功傳送
last_successful_receive: 最後成功接收
last_failed_contact: 最後聯絡失敗
magazine_posting_restricted_to_mods: 僅限版主建立帖子
new_user_description: 此使用者為新使用者(活躍時間少於 %days% 天)
new_magazine_description: 此雜誌為新雜誌(活躍時間少於 %days% 天)
admin_users_active: 活躍
admin_users_inactive: 非活躍
admin_users_suspended: 已停權
admin_users_banned: 已封鎖
user_verify: 啟用帳號
max_image_size: 最大檔案大小
comment_not_found: 找不到留言
bookmark_add_to_list: 將書籤加入 %list%
bookmark_remove_from_list: 從 %list% 移除書籤
bookmark_remove_all: 移除所有書籤
bookmark_add_to_default_list: 將書籤加入預設清單
bookmark_lists: 書籤清單
bookmarks: 書籤
bookmarks_list: '%list% 中的書籤'
count: 數量
is_default: 是否為預設
bookmark_list_is_default: 是否為預設清單
bookmark_list_make_default: 設為預設
bookmark_list_create: 建立
bookmark_list_create_placeholder: 輸入名稱...
bookmark_list_create_label: 清單名稱
bookmarks_list_edit: 編輯書籤清單
bookmark_list_edit: 編輯
bookmark_list_selected_list: 已選清單
table_of_contents: 目錄
search_type_all: 全部
search_type_entry: 帖子
search_type_post: 微鋪
search_type_magazine: 雜誌
search_type_user: 使用者
search_type_actors: 雜誌 + 使用者
search_type_content: 帖子 + 微鋪
select_user: 選擇使用者
new_users_need_approval: 新使用者需經管理員核准後才能登入。
signup_requests: 註冊申請
application_text: 請說明您想加入的原因
signup_requests_header: 註冊申請
signup_requests_paragraph: 這些使用者希望加入您的伺服器。在您核准他們的註冊申請之前,他們無法登入。
flash_application_info: 管理員需要核准您的帳號後您才能登入。您的註冊申請處理完成後,您將會收到一封電子郵件。
email_application_approved_title: 您的註冊申請已獲核准
email_application_approved_body: 您的註冊申請已獲伺服器管理員核准。您現在可以登入伺服器:%siteName% 。
email_application_rejected_title: 您的註冊申請已被拒絕
email_application_rejected_body: 感謝您的關注,但我們遺憾地通知您,您的註冊申請已被拒絕。
email_application_pending: 您的帳號需要管理員核准後才能登入。
email_verification_pending: 您必須驗證您的電子郵件地址才能登入。
show_magazine_domains: 顯示雜誌網域
show_user_domains: 顯示使用者網域
answered: 已回覆
by: 由
front_default_sort: 首頁預設排序
comment_default_sort: 留言預設排序
open_signup_request: 開啟註冊申請
image_lightbox_in_list: 帖子縮圖開啟全螢幕
compact_view_help: 一種邊距較小的精簡檢視,媒體會移至右側。
show_users_avatars_help: 顯示使用者頭像圖片。
show_magazines_icons_help: 顯示雜誌圖示。
show_thumbnails_help: 顯示縮圖圖片。
image_lightbox_in_list_help: 勾選時,點擊縮圖會顯示模態圖片視窗。未勾選時,點擊縮圖將開啟帖子。
show_new_icons: 顯示新圖示
show_new_icons_help: 顯示新雜誌/使用者的圖示(30天內新建立)
magazine_instance_defederated_info: 此雜誌的實例已解除聯邦。因此,該雜誌將不會收到更新。
user_instance_defederated_info: 此使用者的實例已解除聯邦。
flash_thread_instance_banned: 此雜誌的實例已被封鎖。
show_rich_mention: 豐富提及
show_rich_mention_help: 當提及使用者時,呈現使用者元件。這將包含他們的顯示名稱和個人資料圖片。
show_rich_mention_magazine: 豐富雜誌提及
show_rich_mention_magazine_help: 當提及雜誌時,呈現雜誌元件。這將包含它們的顯示名稱和圖示。
show_rich_ap_link: 豐富 AP 連結
show_rich_ap_link_help: 當連結到其他 ActivityPub 內容時,呈現內嵌元件。
attitude: 態度
type_search_magazine: 將搜尋限制於雜誌...
type_search_user: 將搜尋限制於作者...
modlog_type_entry_deleted: 帖子已刪除
modlog_type_entry_restored: 帖子已還原
modlog_type_entry_comment_deleted: 帖子留言已刪除
modlog_type_entry_comment_restored: 帖子留言已還原
modlog_type_entry_pinned: 帖子已置頂
modlog_type_entry_unpinned: 帖子已取消置頂
modlog_type_post_deleted: 微鋪已刪除
modlog_type_post_restored: 微鋪已還原
modlog_type_post_comment_deleted: 微鋪回覆已刪除
modlog_type_post_comment_restored: 微鋪回覆已還原
modlog_type_ban: 使用者已被雜誌封鎖
modlog_type_moderator_add: 雜誌版主已新增
modlog_type_moderator_remove: 雜誌版主已移除
everyone: 所有人
nobody: 無人
followers_only: 僅限追蹤者
direct_message_setting_label: 誰可以向您發送私訊
delete_magazine_icon: 刪除雜誌圖示
flash_magazine_theme_icon_detached_success: 雜誌圖示已成功刪除
delete_magazine_banner: 刪除雜誌橫幅
flash_magazine_theme_banner_detached_success: 雜誌橫幅已成功刪除
federation_uses_allowlist: 對聯邦使用允許清單
defederating_instance: 正在與實例 %i 解除聯邦
their_user_follows: 來自其實例並追蹤我們實例用戶的用戶數量
our_user_follows: 來自我們實例並追蹤其實例用戶的用戶數量
their_magazine_subscriptions: 來自其實例並訂閱我們實例雜誌的用戶數量
our_magazine_subscriptions: 我們實例中訂閱來自其實例雜誌的用戶數量
confirm_defederation: 確認解除聯邦
flash_error_defederation_must_confirm: 您必須確認解除聯邦
allowed_instances: 允許的實例
btn_deny: 拒絕
btn_allow: 允許
ban_instance: 封鎖實例
allow_instance: 允許實例
federation_page_use_allowlist_help:
如果使用允許清單,此實例將僅與明確允許的實例進行聯邦。否則,此實例將與所有實例進行聯邦,除了那些被封鎖的實例。
you_have_been_banned_from_magazine: 您已被雜誌 %m 封鎖。
you_have_been_banned_from_magazine_permanently: 您已被雜誌 %m 永久封鎖。
you_are_no_longer_banned_from_magazine: 您已不再被雜誌 %m 封鎖。
front_default_content: 首頁預設檢視
default_content_default: 伺服器預設(帖子)
default_content_combined: 帖子 + 微鋪
default_content_threads: 帖子
default_content_microblog: 微鋪
combined: 合併
sidebar_sections_random_local_only: 將「隨機帖子/鋪文」側邊欄區塊限制為僅限本地
sidebar_sections_users_local_only: 將「活躍用戶」側邊欄區塊限制為僅限本地
random_local_only_performance_warning: 啟用「僅限本地隨機」可能會對 SQL 效能造成影響。
================================================
FILE: translations/security.en.yaml
================================================
Your account is not active.: Your account is not active.
Your account has been banned.: Your account has been banned.
================================================
FILE: webpack.config.js
================================================
const Encore = require('@symfony/webpack-encore');
//const sass = require('sass');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
.copyFiles({
from: './assets/images',
to: 'images/[path][name].[ext]'
})
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/app.js')
.addEntry('email', './assets/email.js')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// enables the Symfony UX Stimulus bridge (used in assets/stimulus_bootstrap.js)
.enableStimulusBridge('./assets/controllers.json')
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
// Displays build status system notifications to the user
// .enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// configure Babel
// .configureBabel((config) => {
// config.plugins.push('@babel/a-babel-plugin');
// })
// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.38';
})
// enables Sass/SCSS support
.enableSassLoader(function(options) {
// https://sass-lang.com/documentation/js-api/interfaces/options/
// Uncomment this line (and the "require" at the top) to use "pkg:" URLs in Sass
//options.sassOptions = {importers: [new sass.NodePackageImporter()]};
})
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment if you use React
//.enableReactPreset()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();