mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge commit '400c1f3e8ec0ffd33ad30d9334b9210cdb89b14c' into glitch-soc/merge-upstream
This commit is contained in:
27
CHANGELOG.md
27
CHANGELOG.md
@@ -2,6 +2,33 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.5] - 2026-01-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
|
||||
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
|
||||
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
|
||||
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
|
||||
|
||||
### Changed
|
||||
|
||||
- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire)
|
||||
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
|
||||
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
|
||||
- Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436 and #37474 by @mjankowski and @shleeable)
|
||||
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
|
||||
- Fix missing URI scheme test in `QuoteRequest` handling (#37425 by @MegaManSec)
|
||||
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
|
||||
- Fix URI generation for reblogs by accounts with numerical ActivityPub identifiers (#37415 by @oneiros)
|
||||
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
|
||||
- Fix emoji with variant selector not being rendered properly (#37320 by @ChaosExAnima)
|
||||
- Fix mobile admin sidebar displaying under batch table toolbar (#37307 by @diondiondion)
|
||||
|
||||
## [4.5.4] - 2026-01-07
|
||||
|
||||
### Security
|
||||
|
||||
@@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
|
||||
### Additional documentation
|
||||
|
||||
- [Mastodon documentation](https://docs.joinmastodon.org/)
|
||||
|
||||
## Size limits
|
||||
|
||||
Mastodon imposes a few hard limits on federated content.
|
||||
These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons.
|
||||
The following table attempts to summary those limits.
|
||||
|
||||
| Limited property | Size limit | Consequence of exceeding the limit |
|
||||
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
|
||||
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
|
||||
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
|
||||
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
|
||||
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
|
||||
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
|
||||
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
|
||||
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
|
||||
| Account `attributionDomains` | 256 | List will be truncated |
|
||||
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
|
||||
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FeaturedCollectionsController < ApplicationController
|
||||
include SignatureAuthentication
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
|
||||
PER_PAGE = 5
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
before_action :check_feature_enabled
|
||||
before_action :require_account_signature!, if: -> { authorized_fetch_mode? }
|
||||
before_action :set_collections
|
||||
|
||||
skip_around_action :set_locale
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
|
||||
render json: collection_presenter,
|
||||
serializer: ActivityPub::CollectionSerializer,
|
||||
adapter: ActivityPub::Adapter,
|
||||
content_type: 'application/activity+json'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_collections
|
||||
authorize @account, :index_collections?
|
||||
@collections = @account.collections.page(params[:page]).per(PER_PAGE)
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page].present?
|
||||
end
|
||||
|
||||
def next_page_url
|
||||
ap_account_featured_collections_url(@account, page: @collections.next_page) if @collections.respond_to?(:next_page)
|
||||
end
|
||||
|
||||
def prev_page_url
|
||||
ap_account_featured_collections_url(@account, page: @collections.prev_page) if @collections.respond_to?(:prev_page)
|
||||
end
|
||||
|
||||
def collection_presenter
|
||||
if page_requested?
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: ap_account_featured_collections_url(@account, page: params.fetch(:page, 1)),
|
||||
type: :unordered,
|
||||
size: @account.collections.count,
|
||||
items: @collections,
|
||||
part_of: ap_account_featured_collections_url(@account),
|
||||
next: next_page_url,
|
||||
prev: prev_page_url
|
||||
)
|
||||
else
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: ap_account_featured_collections_url(@account),
|
||||
type: :unordered,
|
||||
size: @account.collections.count,
|
||||
first: ap_account_featured_collections_url(@account, page: 1)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def check_feature_enabled
|
||||
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@
|
||||
class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
include JsonLdHelper
|
||||
|
||||
before_action :skip_large_payload
|
||||
before_action :skip_unknown_actor_activity
|
||||
before_action :require_actor_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
@@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
|
||||
private
|
||||
|
||||
def skip_large_payload
|
||||
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
|
||||
end
|
||||
|
||||
def skip_unknown_actor_activity
|
||||
head 202 if unknown_affected_account?
|
||||
end
|
||||
|
||||
@@ -81,11 +81,11 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
end
|
||||
|
||||
def collection_creation_params
|
||||
params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: [])
|
||||
params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name, account_ids: [])
|
||||
end
|
||||
|
||||
def collection_update_params
|
||||
params.permit(:name, :description, :sensitive, :discoverable, :tag_name)
|
||||
params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name)
|
||||
end
|
||||
|
||||
def check_feature_enabled
|
||||
|
||||
@@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
end
|
||||
|
||||
def set_push_subscription
|
||||
@push_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
|
||||
@@ -128,15 +128,18 @@ export default function api(withAuthorization = true) {
|
||||
}
|
||||
|
||||
type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`;
|
||||
type RequestParamsOrData = Record<string, unknown>;
|
||||
type RequestParamsOrData<T = unknown> = T | Record<string, unknown>;
|
||||
|
||||
export async function apiRequest<ApiResponse = unknown>(
|
||||
export async function apiRequest<
|
||||
ApiResponse = unknown,
|
||||
ApiParamsOrData = unknown,
|
||||
>(
|
||||
method: Method,
|
||||
url: string,
|
||||
args: {
|
||||
signal?: AbortSignal;
|
||||
params?: RequestParamsOrData;
|
||||
data?: RequestParamsOrData;
|
||||
params?: RequestParamsOrData<ApiParamsOrData>;
|
||||
data?: RequestParamsOrData<ApiParamsOrData>;
|
||||
timeout?: number;
|
||||
} = {},
|
||||
) {
|
||||
@@ -149,30 +152,30 @@ export async function apiRequest<ApiResponse = unknown>(
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function apiRequestGet<ApiResponse = unknown>(
|
||||
export async function apiRequestGet<ApiResponse = unknown, ApiParams = unknown>(
|
||||
url: ApiUrl,
|
||||
params?: RequestParamsOrData,
|
||||
params?: RequestParamsOrData<ApiParams>,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('GET', url, { params });
|
||||
}
|
||||
|
||||
export async function apiRequestPost<ApiResponse = unknown>(
|
||||
export async function apiRequestPost<ApiResponse = unknown, ApiData = unknown>(
|
||||
url: ApiUrl,
|
||||
data?: RequestParamsOrData,
|
||||
data?: RequestParamsOrData<ApiData>,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('POST', url, { data });
|
||||
}
|
||||
|
||||
export async function apiRequestPut<ApiResponse = unknown>(
|
||||
export async function apiRequestPut<ApiResponse = unknown, ApiData = unknown>(
|
||||
url: ApiUrl,
|
||||
data?: RequestParamsOrData,
|
||||
data?: RequestParamsOrData<ApiData>,
|
||||
) {
|
||||
return apiRequest<ApiResponse>('PUT', url, { data });
|
||||
}
|
||||
|
||||
export async function apiRequestDelete<ApiResponse = unknown>(
|
||||
url: ApiUrl,
|
||||
params?: RequestParamsOrData,
|
||||
) {
|
||||
export async function apiRequestDelete<
|
||||
ApiResponse = unknown,
|
||||
ApiParams = unknown,
|
||||
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
||||
return apiRequest<ApiResponse>('DELETE', url, { params });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, useRef, useCallback, useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
@@ -15,7 +16,9 @@ export const DomainPill: React.FC<{
|
||||
domain: string;
|
||||
username: string;
|
||||
isSelf: boolean;
|
||||
}> = ({ domain, username, isSelf }) => {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}> = ({ domain, username, isSelf, children, className }) => {
|
||||
const accessibilityId = useId();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@@ -32,7 +35,9 @@ export const DomainPill: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={classNames('account__domain-pill', { active: open })}
|
||||
className={classNames('account__domain-pill', className, {
|
||||
active: open,
|
||||
})}
|
||||
ref={triggerRef}
|
||||
onClick={handleClick}
|
||||
aria-expanded={open}
|
||||
@@ -40,6 +45,7 @@ export const DomainPill: React.FC<{
|
||||
type='button'
|
||||
>
|
||||
{domain}
|
||||
{children}
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
||||
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
import { AccountName } from './account_name';
|
||||
import { AccountBadges } from './badges';
|
||||
import { AccountButtons } from './buttons';
|
||||
import { FamiliarFollowers } from './familiar_followers';
|
||||
@@ -28,6 +25,7 @@ import { AccountInfo } from './info';
|
||||
import { MemorialNote } from './memorial_note';
|
||||
import { MovedNote } from './moved_note';
|
||||
import { AccountNumberFields } from './number_fields';
|
||||
import redesignClasses from './redesign.module.scss';
|
||||
import { AccountTabs } from './tabs';
|
||||
|
||||
const titleFromAccount = (account: Account) => {
|
||||
@@ -47,7 +45,6 @@ export const AccountHeader: React.FC<{
|
||||
hideTabs?: boolean;
|
||||
}> = ({ accountId, hideTabs }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const relationship = useAppSelector((state) =>
|
||||
state.relationships.get(accountId),
|
||||
@@ -85,8 +82,6 @@ export const AccountHeader: React.FC<{
|
||||
|
||||
const suspendedOrHidden = hidden || account.suspended;
|
||||
const isLocal = !account.acct.includes('@');
|
||||
const username = account.acct.split('@')[0];
|
||||
const domain = isLocal ? localDomain : account.acct.split('@')[1];
|
||||
|
||||
return (
|
||||
<div className='account-timeline__header'>
|
||||
@@ -133,38 +128,27 @@ export const AccountHeader: React.FC<{
|
||||
/>
|
||||
</a>
|
||||
|
||||
<AccountButtons
|
||||
accountId={accountId}
|
||||
className='account__header__buttons--desktop'
|
||||
/>
|
||||
{!isRedesignEnabled() && (
|
||||
<AccountButtons
|
||||
accountId={accountId}
|
||||
className='account__header__buttons--desktop'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
<h1>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
<small>
|
||||
<span>
|
||||
@{username}
|
||||
<span className='invisible'>@{domain}</span>
|
||||
</span>
|
||||
<DomainPill
|
||||
username={username ?? ''}
|
||||
domain={domain ?? ''}
|
||||
isSelf={me === account.id}
|
||||
/>
|
||||
{account.locked && (
|
||||
<Icon
|
||||
id='lock'
|
||||
icon={LockIcon}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'account.locked_info',
|
||||
defaultMessage:
|
||||
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</small>
|
||||
</h1>
|
||||
<div
|
||||
className={classNames(
|
||||
'account__header__tabs__name',
|
||||
isRedesignEnabled() && redesignClasses.nameWrapper,
|
||||
)}
|
||||
>
|
||||
<AccountName
|
||||
accountId={accountId}
|
||||
className={classNames(
|
||||
isRedesignEnabled() && redesignClasses.name,
|
||||
)}
|
||||
/>
|
||||
{isRedesignEnabled() && <AccountButtons accountId={accountId} />}
|
||||
</div>
|
||||
|
||||
<AccountBadges accountId={accountId} />
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
|
||||
import { DomainPill } from '../../account/components/domain_pill';
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
export const AccountName: FC<{ accountId: string; className?: string }> = ({
|
||||
accountId,
|
||||
className,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
const me = useAppSelector((state) => state.meta.get('me') as string);
|
||||
const localDomain = useAppSelector(
|
||||
(state) => state.meta.get('domain') as string,
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [username = '', domain = localDomain] = account.acct.split('@');
|
||||
|
||||
return (
|
||||
<h1 className={className}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
<small>
|
||||
<span>
|
||||
@{username}
|
||||
{isRedesignEnabled() && '@'}
|
||||
<span className='invisible'>
|
||||
{!isRedesignEnabled() && '@'}
|
||||
{domain}
|
||||
</span>
|
||||
</span>
|
||||
<DomainPill
|
||||
username={username}
|
||||
domain={domain}
|
||||
isSelf={me === account.id}
|
||||
className={(isRedesignEnabled() && classes.domainPill) || ''}
|
||||
>
|
||||
{isRedesignEnabled() && <Icon id='info' icon={InfoIcon} />}
|
||||
</DomainPill>
|
||||
{!isRedesignEnabled() && account.locked && (
|
||||
<Icon
|
||||
id='lock'
|
||||
icon={LockIcon}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'account.locked_info',
|
||||
defaultMessage:
|
||||
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</small>
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,44 @@
|
||||
.nameWrapper {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
font-size: 22px;
|
||||
white-space: initial;
|
||||
text-overflow: initial;
|
||||
line-height: normal;
|
||||
|
||||
:global(.icon-info) {
|
||||
margin-left: 2px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides .account__header__tabs__name h1 small
|
||||
h1.name > small {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.domainPill {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
font-weight: initial;
|
||||
|
||||
&:global(.active) {
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldList {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
"about.blocks": "Modereeritavad serverid",
|
||||
"about.contact": "Kontakt:",
|
||||
"about.default_locale": "Vaikimisi",
|
||||
"about.disclaimer": "Mastodon on tasuta ja vaba tarkvara ning Mastodon gGmbH kaubamärk.",
|
||||
"about.disclaimer": "Mastodon on vaba, tasuta ja avatud lähtekoodiga tarkvara ning Mastodon gGmbH kaubamärk.",
|
||||
"about.domain_blocks.no_reason_available": "Põhjus on teadmata",
|
||||
"about.domain_blocks.preamble": "Mastodon lubab tavaliselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest fediversumi serverist. Need on erandid, mis on paika pandud sellel kindlal serveril.",
|
||||
"about.domain_blocks.silenced.explanation": "Sa ei näe üldiselt profiile ja sisu sellelt serverilt, kui sa just tahtlikult seda ei otsi või jälgimise moel nõusolekut ei anna.",
|
||||
"about.domain_blocks.preamble": "Mastodon lubab üldiselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest födiversumi serverist. Need on erandid, mis kehtivad selles kindlas serveris.",
|
||||
"about.domain_blocks.silenced.explanation": "Sa üldjuhul ei näe profiile ja sisu sellest serverist, kui sa just tahtlikult neid ei otsi või jälgimise moel nõusolekut ei anna.",
|
||||
"about.domain_blocks.silenced.title": "Piiratud",
|
||||
"about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse interaktsiooni või kirjavahetuse selle serveri kasutajatega võimatuks.",
|
||||
"about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse suhestumise või infovahetuse selle serveri kasutajatega võimatuks.",
|
||||
"about.domain_blocks.suspended.title": "Peatatud",
|
||||
"about.language_label": "Keel",
|
||||
"about.not_available": "See info ei ole sellel serveril saadavaks tehtud.",
|
||||
"about.not_available": "See info ei ole selles serveris saadavaks tehtud.",
|
||||
"about.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}",
|
||||
"about.rules": "Serveri reeglid",
|
||||
"account.account_note_header": "Isiklik märge",
|
||||
@@ -18,19 +18,19 @@
|
||||
"account.badges.bot": "Robot",
|
||||
"account.badges.group": "Grupp",
|
||||
"account.block": "Blokeeri @{name}",
|
||||
"account.block_domain": "Peida kõik domeenist {domain}",
|
||||
"account.block_domain": "Blokeeri kõik domeenist {domain}",
|
||||
"account.block_short": "Blokeerimine",
|
||||
"account.blocked": "Blokeeritud",
|
||||
"account.blocking": "Blokeeritud kasutaja",
|
||||
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
||||
"account.copy": "Kopeeri profiili link",
|
||||
"account.direct": "Maini privaatselt @{name}",
|
||||
"account.disable_notifications": "Peata teavitused @{name} postitustest",
|
||||
"account.disable_notifications": "Ära teavita, kui @{name} postitab",
|
||||
"account.domain_blocking": "Blokeeritud domeen",
|
||||
"account.edit_profile": "Muuda profiili",
|
||||
"account.edit_profile_short": "Muuda",
|
||||
"account.enable_notifications": "Teavita mind @{name} postitustest",
|
||||
"account.endorse": "Too profiilil esile",
|
||||
"account.enable_notifications": "Teavita mind, kui {name} postitab",
|
||||
"account.endorse": "Too profiilis esile",
|
||||
"account.familiar_followers_many": "Jälgijateks {name1}, {name2} ja veel {othersCount, plural, one {üks kasutaja, keda tead} other {# kasutajat, keda tead}}",
|
||||
"account.familiar_followers_one": "Jälgijaks {name1}",
|
||||
"account.familiar_followers_two": "Jälgijateks {name1} ja {name2}",
|
||||
@@ -57,11 +57,12 @@
|
||||
"account.go_to_profile": "Mine profiilile",
|
||||
"account.hide_reblogs": "Peida @{name} jagamised",
|
||||
"account.in_memoriam": "In Memoriam.",
|
||||
"account.joined_long": "Liitus {date}",
|
||||
"account.joined_short": "Liitus",
|
||||
"account.languages": "Muuda tellitud keeli",
|
||||
"account.link_verified_on": "Selle lingi autorsust kontrolliti {date}",
|
||||
"account.locked_info": "Selle konto privaatsussätteks on lukustatud. Omanik vaatab käsitsi üle, kes teda jälgida saab.",
|
||||
"account.media": "Meedia",
|
||||
"account.media": "Meedium",
|
||||
"account.mention": "Maini @{name}",
|
||||
"account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:",
|
||||
"account.mute": "Summuta @{name}",
|
||||
@@ -81,20 +82,22 @@
|
||||
"account.share": "Jaga @{name} profiili",
|
||||
"account.show_reblogs": "Näita @{name} jagamisi",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}",
|
||||
"account.unblock": "Eemalda blokeering @{name}",
|
||||
"account.unblock_domain": "Tee {domain} nähtavaks",
|
||||
"account.unblock": "Lõpeta {name} kasutaja blokeerimine",
|
||||
"account.unblock_domain": "Lõpeta {domain} domeeni blokeerimine",
|
||||
"account.unblock_domain_short": "Lõpeta blokeerimine",
|
||||
"account.unblock_short": "Eemalda blokeering",
|
||||
"account.unblock_short": "Lõpeta blokeerimine",
|
||||
"account.unendorse": "Ära kuva profiilil",
|
||||
"account.unfollow": "Jälgid",
|
||||
"account.unfollow": "Ära jälgi",
|
||||
"account.unmute": "Lõpeta {name} kasutaja summutamine",
|
||||
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
|
||||
"account.unmute_short": "Lõpeta summutamine",
|
||||
"account_fields_modal.close": "Sulge",
|
||||
"account_fields_modal.title": "Kasutaja teave: {name}",
|
||||
"account_note.placeholder": "Klõpsa märke lisamiseks",
|
||||
"admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist",
|
||||
"admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist",
|
||||
"admin.dashboard.retention.average": "Keskmine",
|
||||
"admin.dashboard.retention.cohort": "Registreerumiskuu",
|
||||
"admin.dashboard.retention.cohort": "Liitumiskuu",
|
||||
"admin.dashboard.retention.cohort_size": "Uued kasutajad",
|
||||
"admin.impact_report.instance_accounts": "Kontode profiilid, mille see kustutaks",
|
||||
"admin.impact_report.instance_followers": "Jälgijad, kelle meie kasutajad kaotaks",
|
||||
@@ -103,11 +106,11 @@
|
||||
"alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.",
|
||||
"alert.rate_limited.title": "Kiiruspiirang",
|
||||
"alert.unexpected.message": "Tekkis ootamatu viga.",
|
||||
"alert.unexpected.title": "Oih!",
|
||||
"alt_text_badge.title": "Alternatiivtekst",
|
||||
"alt_text_modal.add_alt_text": "Lisa alt-tekst",
|
||||
"alert.unexpected.title": "Vaat kus lops!",
|
||||
"alt_text_badge.title": "Selgitustekst",
|
||||
"alt_text_modal.add_alt_text": "Lisa selgitustekst",
|
||||
"alt_text_modal.add_text_from_image": "Lisa tekst pildilt",
|
||||
"alt_text_modal.cancel": "Tühista",
|
||||
"alt_text_modal.cancel": "Katkesta",
|
||||
"alt_text_modal.change_thumbnail": "Muuda pisipilti",
|
||||
"alt_text_modal.describe_for_people_with_hearing_impairments": "Kirjelda seda kuulmispuudega inimeste jaoks…",
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Kirjelda seda nägemispuudega inimeste jaoks…",
|
||||
@@ -119,7 +122,7 @@
|
||||
"annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.",
|
||||
"annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis",
|
||||
"annual_report.nav_item.badge": "Uus",
|
||||
"annual_report.shared_page.donate": "Anneta",
|
||||
"annual_report.shared_page.donate": "Toeta rahaliselt",
|
||||
"annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt",
|
||||
"annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.",
|
||||
"annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.",
|
||||
@@ -589,6 +592,7 @@
|
||||
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
||||
"loading_indicator.label": "Laadimine…",
|
||||
"media_gallery.hide": "Peida",
|
||||
"minicard.more_items": "+{count}",
|
||||
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
||||
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
|
||||
"mute_modal.hide_options": "Peida valikud",
|
||||
@@ -767,7 +771,7 @@
|
||||
"onboarding.profile.upload_avatar": "Laadi üles profiilipilt",
|
||||
"onboarding.profile.upload_header": "Laadi üles profiili päis",
|
||||
"password_confirmation.exceeds_maxlength": "Salasõnakinnitus on pikem kui salasõna maksimumpikkus",
|
||||
"password_confirmation.mismatching": "Salasõnakinnitus ei sobi kokku",
|
||||
"password_confirmation.mismatching": "Salasõnad ei klapi",
|
||||
"picture_in_picture.restore": "Pane tagasi",
|
||||
"poll.closed": "Suletud",
|
||||
"poll.refresh": "Värskenda",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"account.go_to_profile": "Profile git",
|
||||
"account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle",
|
||||
"account.in_memoriam": "Hatırasına.",
|
||||
"account.joined_long": "{date} tarihinde katıldı",
|
||||
"account.joined_short": "Katıldı",
|
||||
"account.languages": "Abone olunan dilleri değiştir",
|
||||
"account.link_verified_on": "Bu bağlantının sahipliği {date} tarihinde denetlendi",
|
||||
@@ -90,6 +91,8 @@
|
||||
"account.unmute": "@{name} adlı kişinin sesini aç",
|
||||
"account.unmute_notifications_short": "Bildirimlerin sesini aç",
|
||||
"account.unmute_short": "Susturmayı kaldır",
|
||||
"account_fields_modal.close": "Kapat",
|
||||
"account_fields_modal.title": "{name} bilgileri",
|
||||
"account_note.placeholder": "Not eklemek için tıklayın",
|
||||
"admin.dashboard.daily_retention": "Kayıttan sonra günlük kullanıcı saklama oranı",
|
||||
"admin.dashboard.monthly_retention": "Kayıttan sonra aylık kullanıcı saklama oranı",
|
||||
@@ -589,6 +592,7 @@
|
||||
"load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}",
|
||||
"loading_indicator.label": "Yükleniyor…",
|
||||
"media_gallery.hide": "Gizle",
|
||||
"minicard.more_items": "+{count}",
|
||||
"moved_to_account_banner.text": "{disabledAccount} hesabınız, {movedToAccount} hesabına taşıdığınız için şu an devre dışı.",
|
||||
"mute_modal.hide_from_notifications": "Bildirimlerde gizle",
|
||||
"mute_modal.hide_options": "Seçenekleri gizle",
|
||||
|
||||
@@ -5,6 +5,7 @@ class ActivityPub::Activity
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
MAX_JSON_SIZE = 1.megabyte
|
||||
SUPPORTED_TYPES = %w(Note Question).freeze
|
||||
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
@status = Status.find_by(uri: object_uri, account_id: @account.id)
|
||||
|
||||
# Ignore updates for old unknown objects, since those are updates we are not interested in
|
||||
return if @status.nil? && object_too_old?
|
||||
# Also ignore unknown objects from suspended users for the same reasons
|
||||
return if @status.nil? && (@account.suspended? || object_too_old?)
|
||||
|
||||
# We may be getting `Create` and `Update` out of order
|
||||
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
class ActivityPub::Parser::PollParser
|
||||
include JsonLdHelper
|
||||
|
||||
# Limit the number of items for performance purposes.
|
||||
# We truncate rather than error out to avoid missing the post entirely.
|
||||
MAX_ITEMS = 500
|
||||
|
||||
def initialize(json)
|
||||
@json = json
|
||||
end
|
||||
@@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser
|
||||
private
|
||||
|
||||
def items
|
||||
@json['anyOf'] || @json['oneOf']
|
||||
(@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,6 +81,13 @@ class Account < ApplicationRecord
|
||||
DISPLAY_NAME_LENGTH_LIMIT = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
||||
NOTE_LENGTH_LIMIT = (ENV['MAX_BIO_CHARS'] || 500).to_i
|
||||
|
||||
# Hard limits for federated content
|
||||
USERNAME_LENGTH_HARD_LIMIT = 2048
|
||||
DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048
|
||||
NOTE_LENGTH_HARD_LIMIT = 20.kilobytes
|
||||
ATTRIBUTION_DOMAINS_HARD_LIMIT = 256
|
||||
ALSO_KNOWN_AS_HARD_LIMIT = 256
|
||||
|
||||
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
|
||||
|
||||
include Attachmentable # Load prior to Avatar & Header concerns
|
||||
@@ -114,7 +121,7 @@ class Account < ApplicationRecord
|
||||
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
||||
|
||||
# Remote user validations, also applies to internal actors
|
||||
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
|
||||
validates :username, format: { with: USERNAME_ONLY_RE }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
|
||||
|
||||
# Remote user validations
|
||||
validates :uri, presence: true, unless: :local?, on: :create
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# description :text not null
|
||||
# discoverable :boolean not null
|
||||
# item_count :integer default(0), not null
|
||||
# language :string
|
||||
# local :boolean not null
|
||||
# name :string not null
|
||||
# original_number_of_items :integer
|
||||
@@ -36,6 +37,7 @@ class Collection < ApplicationRecord
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal: 0 },
|
||||
if: :remote?
|
||||
validates :language, language: { if: :local?, allow_nil: true }
|
||||
validate :tag_is_usable
|
||||
validate :items_do_not_exceed_limit
|
||||
|
||||
|
||||
@@ -164,6 +164,13 @@ module Account::Interactions
|
||||
end
|
||||
end
|
||||
|
||||
def blocking_or_domain_blocking?(other_account)
|
||||
return true if blocking?(other_account)
|
||||
return false if other_account.domain.blank?
|
||||
|
||||
domain_blocking?(other_account.domain)
|
||||
end
|
||||
|
||||
def muting?(other_account)
|
||||
other_id = other_account.is_a?(Account) ? other_account.id : other_account
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class CustomEmoji < ApplicationRecord
|
||||
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
|
||||
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
|
||||
MINIMUM_SHORTCODE_SIZE = 2
|
||||
MAX_SHORTCODE_SIZE = 128
|
||||
MAX_FEDERATED_SHORTCODE_SIZE = 2048
|
||||
|
||||
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
|
||||
|
||||
@@ -48,7 +50,8 @@ class CustomEmoji < ApplicationRecord
|
||||
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true
|
||||
validates_attachment_size :image, less_than: LIMIT, unless: :local?
|
||||
validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :local?
|
||||
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE }
|
||||
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE }
|
||||
validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local?
|
||||
|
||||
scope :local, -> { where(domain: nil) }
|
||||
scope :remote, -> { where.not(domain: nil) }
|
||||
|
||||
@@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord
|
||||
|
||||
EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze
|
||||
|
||||
TITLE_LENGTH_LIMIT = 256
|
||||
|
||||
include Expireable
|
||||
include Redisable
|
||||
|
||||
@@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord
|
||||
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
||||
|
||||
validates :title, :context, presence: true
|
||||
validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
|
||||
validate :context_must_be_valid
|
||||
|
||||
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }
|
||||
|
||||
@@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord
|
||||
|
||||
belongs_to :custom_filter
|
||||
|
||||
validates :keyword, presence: true
|
||||
KEYWORD_LENGTH_LIMIT = 512
|
||||
|
||||
validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT }
|
||||
|
||||
alias_attribute :phrase, :keyword
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class List < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
PER_ACCOUNT_LIMIT = 50
|
||||
TITLE_LENGTH_LIMIT = 256
|
||||
|
||||
enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true
|
||||
|
||||
@@ -26,7 +27,7 @@ class List < ApplicationRecord
|
||||
has_many :accounts, through: :list_accounts
|
||||
has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account
|
||||
|
||||
validates :title, presence: true
|
||||
validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT }
|
||||
|
||||
validate :validate_account_lists_limit, on: :create
|
||||
|
||||
|
||||
@@ -68,4 +68,8 @@ class AccountPolicy < ApplicationPolicy
|
||||
def feature?
|
||||
record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record)
|
||||
end
|
||||
|
||||
def index_collections?
|
||||
current_account.nil? || !record.blocking_or_domain_blocking?(current_account)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
def show?
|
||||
current_account.nil? || (!owner_blocking? && !owner_blocking_domain?)
|
||||
current_account.nil? || !owner.blocking_or_domain_blocking?(current_account)
|
||||
end
|
||||
|
||||
def create?
|
||||
@@ -27,18 +27,6 @@ class CollectionPolicy < ApplicationPolicy
|
||||
current_account == owner
|
||||
end
|
||||
|
||||
def owner_blocking_domain?
|
||||
return false if current_account.nil? || current_account.domain.nil?
|
||||
|
||||
owner.domain_blocking?(current_account.domain)
|
||||
end
|
||||
|
||||
def owner_blocking?
|
||||
return false if current_account.nil?
|
||||
|
||||
current_account.blocked_by?(owner)
|
||||
end
|
||||
|
||||
def owner
|
||||
record.account
|
||||
end
|
||||
|
||||
@@ -19,6 +19,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
:discoverable, :indexable, :published, :memorial
|
||||
|
||||
attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
|
||||
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
|
||||
|
||||
@@ -177,6 +178,12 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
}
|
||||
end
|
||||
|
||||
def featured_collections
|
||||
return nil if instance_actor?
|
||||
|
||||
ap_account_featured_collections_url(object.id)
|
||||
end
|
||||
|
||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
|
||||
end
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer
|
||||
ActivityPub::HashtagSerializer
|
||||
when 'ActivityPub::CollectionPresenter'
|
||||
ActivityPub::CollectionSerializer
|
||||
when 'Collection'
|
||||
ActivityPub::FeaturedCollectionSerializer
|
||||
when 'String'
|
||||
StringSerializer
|
||||
else
|
||||
|
||||
@@ -17,9 +17,12 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
attributes :id, :type, :total_items, :name, :summary, :attributed_to,
|
||||
attributes :id, :type, :total_items, :name, :attributed_to,
|
||||
:sensitive, :discoverable, :published, :updated
|
||||
|
||||
attribute :summary, unless: :language_present?
|
||||
attribute :summary_map, if: :language_present?
|
||||
|
||||
has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer
|
||||
|
||||
has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer
|
||||
@@ -36,6 +39,10 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
||||
object.description
|
||||
end
|
||||
|
||||
def summary_map
|
||||
{ object.language => object.description }
|
||||
end
|
||||
|
||||
def attributed_to
|
||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||
end
|
||||
@@ -51,4 +58,8 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
||||
def updated
|
||||
object.updated_at.iso8601
|
||||
end
|
||||
|
||||
def language_present?
|
||||
object.language.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::BaseCollectionSerializer < ActiveModel::Serializer
|
||||
attributes :id, :uri, :name, :description, :local, :sensitive,
|
||||
attributes :id, :uri, :name, :description, :language, :local, :sensitive,
|
||||
:discoverable, :item_count, :created_at, :updated_at
|
||||
|
||||
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
|
||||
|
||||
@@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
MAX_PROFILE_FIELDS = 50
|
||||
SUBDOMAINS_RATELIMIT = 10
|
||||
DISCOVERIES_PER_REQUEST = 400
|
||||
|
||||
@@ -124,15 +125,15 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
|
||||
def set_immediate_attributes!
|
||||
@account.featured_collection_url = valid_collection_uri(@json['featured'])
|
||||
@account.display_name = @json['name'] || ''
|
||||
@account.note = @json['summary'] || ''
|
||||
@account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)]
|
||||
@account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)]
|
||||
@account.locked = @json['manuallyApprovesFollowers'] || false
|
||||
@account.fields = property_values || {}
|
||||
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
|
||||
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) }
|
||||
@account.discoverable = @json['discoverable'] || false
|
||||
@account.indexable = @json['indexable'] || false
|
||||
@account.memorial = @json['memorial'] || false
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) }
|
||||
end
|
||||
|
||||
def set_fetchable_key!
|
||||
@@ -253,7 +254,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
def property_values
|
||||
return unless @json['attachment'].is_a?(Array)
|
||||
|
||||
as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
|
||||
as_array(@json['attachment'])
|
||||
.select { |attachment| attachment['type'] == 'PropertyValue' }
|
||||
.take(MAX_PROFILE_FIELDS)
|
||||
.map { |attachment| attachment.slice('name', 'value') }
|
||||
end
|
||||
|
||||
def mismatching_origin?(url)
|
||||
|
||||
@@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService
|
||||
@account = status.account
|
||||
@options = options
|
||||
|
||||
return if @status.proper.account.suspended?
|
||||
|
||||
check_race_condition!
|
||||
warm_payload_cache!
|
||||
|
||||
|
||||
@@ -29,23 +29,23 @@ et:
|
||||
title: Kinnita e-postiaadress
|
||||
email_changed:
|
||||
explanation: 'Sinu konto e-postiaadress muudetakse:'
|
||||
extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontost välja lukustatud, võta ühendust oma serveri administraatoriga.
|
||||
extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontole ligipääsu kaotanud, palun võta kohe ühendust oma serveri haldajaga.
|
||||
subject: 'Mastodon: e-post muudetud'
|
||||
title: Uus e-postiaadress
|
||||
password_change:
|
||||
explanation: Konto salasõna on vahetatud.
|
||||
extra: Kui sa ei muutnud oma salasõna, on tõenäoline, et keegi on su kontole ligi pääsenud. Palun muuda viivitamata oma salasõna. Kui sa oma kontole ligi ei pääse, võta ühendust serveri haldajaga.
|
||||
subject: 'Mastodon: salasõna muudetud'
|
||||
title: Salasõna muudetud
|
||||
subject: 'Mastodon: salasõna on muudetud'
|
||||
title: Salasõna on muudetud
|
||||
reconfirmation_instructions:
|
||||
explanation: Kinnita uus aadress, et oma e-posti aadress muuta.
|
||||
extra: Kui see muudatus pole sinu poolt algatatud, palun eira seda kirja. Selle Mastodoni konto e-postiaadress ei muutu enne, kui vajutad üleval olevale lingile.
|
||||
subject: 'Mastodon: kinnita e-postiaadress %{instance} jaoks'
|
||||
title: Kinnita e-postiaadress
|
||||
reset_password_instructions:
|
||||
action: Salasõna muutmine
|
||||
explanation: Kontole on küsitud uut salasõna.
|
||||
extra: Kui see tuleb üllatusena, võib seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna määramist.
|
||||
action: Muuda salasõna
|
||||
explanation: Sa palusid oma kasutajakontole luua uus salasõna.
|
||||
extra: Kui see tuleb üllatusena, võid seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna sisestamist.
|
||||
subject: 'Mastodon: salasõna lähtestamisjuhendid'
|
||||
title: Salasõna lähtestamine
|
||||
two_factor_disabled:
|
||||
|
||||
@@ -142,7 +142,7 @@ et:
|
||||
security: Turvalisus
|
||||
security_measures:
|
||||
only_password: Ainult salasõna
|
||||
password_and_2fa: Salasõna ja 2-etapine autentimine (2FA)
|
||||
password_and_2fa: Salasõna ja kahefaktoriline autentimine (2FA)
|
||||
sensitive: Tundlik sisu
|
||||
sensitized: Märgitud kui tundlik sisu
|
||||
shared_inbox_url: Jagatud sisendkausta URL
|
||||
@@ -292,7 +292,7 @@ et:
|
||||
remove_avatar_user_html: "%{name} eemaldas %{target} avatari"
|
||||
reopen_report_html: "%{name} taasavas raporti %{target}"
|
||||
resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti"
|
||||
reset_password_user_html: "%{name} lähtestas %{target} salasõna"
|
||||
reset_password_user_html: "%{name} lähtestas %{target} kasutaja salasõna"
|
||||
resolve_report_html: "%{name} lahendas raporti %{target}"
|
||||
sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu"
|
||||
silence_account_html: "%{name} piiras %{target} konto"
|
||||
@@ -787,7 +787,7 @@ et:
|
||||
manage_taxonomies: Halda taksonoomiaid
|
||||
manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada teemaviidete seadistusi
|
||||
manage_user_access: Halda kasutajate ligipääsu
|
||||
manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta oma e-posti aadressi ja lähtestada oma parooli
|
||||
manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta nende e-posti aadressi ja lähtestada oma salasõna
|
||||
manage_users: Kasutajate haldamine
|
||||
manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid
|
||||
manage_webhooks: Halda webhook'e
|
||||
@@ -1249,7 +1249,7 @@ et:
|
||||
suffix: Kasutajakontoga saad jälgida inimesi, postitada uudiseid ning pidada kirjavahetust ükskõik millise Mastodoni serveri kasutajatega ja muudki!
|
||||
didnt_get_confirmation: Ei saanud kinnituslinki?
|
||||
dont_have_your_security_key: Pole turvavõtit?
|
||||
forgot_password: Salasõna ununenud?
|
||||
forgot_password: Kas unustasid oma salasõna?
|
||||
invalid_reset_password_token: Salasõna lähtestusvõti on vale või aegunud. Palun taotle uus.
|
||||
link_to_otp: Kaheastmeline kood telefonist või taastekood
|
||||
link_to_webauth: Turvavõtmete seadme kasutamine
|
||||
@@ -1270,7 +1270,7 @@ et:
|
||||
register: Loo konto
|
||||
registration_closed: "%{instance} ei võta vastu uusi liikmeid"
|
||||
resend_confirmation: Saada kinnituslink uuesti
|
||||
reset_password: Salasõna lähtestamine
|
||||
reset_password: Lähtesta salasõna
|
||||
rules:
|
||||
accept: Nõus
|
||||
back: Tagasi
|
||||
@@ -1280,7 +1280,7 @@ et:
|
||||
title: Mõned põhireeglid.
|
||||
title_invited: Oled kutsutud.
|
||||
security: Turvalisus
|
||||
set_new_password: Uue salasõna määramine
|
||||
set_new_password: Sisesta uus salasõna
|
||||
setup:
|
||||
email_below_hint_html: Kontrolli rämpsposti kausta või palu uue kirja saatmist. Kui sinu e-posti aadress on vale, siis saad seda parandada.
|
||||
email_settings_hint_html: Klõpsa aadressile %{email} saadetud linki, et alustada Mastodoni kasutamist. Me oleme ootel.
|
||||
@@ -1316,9 +1316,9 @@ et:
|
||||
title: Autori tunnustamine
|
||||
challenge:
|
||||
confirm: Jätka
|
||||
hint_html: "<strong>Nõuanne:</strong> Me ei küsi salasõna uuesti järgmise tunni jooksul."
|
||||
hint_html: "<strong>Nõuanne:</strong> Me ei küsi sinu salasõna uuesti järgmise tunni jooksul."
|
||||
invalid_password: Vigane salasõna
|
||||
prompt: Jätkamiseks salasõna veelkord
|
||||
prompt: Jätkamiseks korda salasõna
|
||||
color_scheme:
|
||||
auto: Auto
|
||||
dark: Tume
|
||||
@@ -1627,7 +1627,7 @@ et:
|
||||
password: salasõna
|
||||
sign_in_token: e-posti turvvakood
|
||||
webauthn: turvavõtmed
|
||||
description_html: Kui paistab tundmatuid tegevusi, tuleks vahetada salasõna ja aktiveerida kaheastmeline autentimine.
|
||||
description_html: Kui paistab tundmatuid tegevusi, palun vaheta salasõna ja aktiveeri kaheastmeline autentimine.
|
||||
empty: Autentimisajalugu pole saadaval
|
||||
failed_sign_in_html: Nurjunud sisenemine meetodiga %{method} aadressilt %{ip} (%{browser})
|
||||
successful_sign_in_html: Edukas sisenemine meetodiga %{method} aadressilt %{ip} (%{browser})
|
||||
|
||||
@@ -51,7 +51,7 @@ et:
|
||||
inbox_url: Kopeeri soovitud sõnumivahendusserveri avalehe võrguaadress
|
||||
irreversible: Filtreeritud postitused kaovad taastamatult, isegi kui filter on hiljem eemaldatud
|
||||
locale: Kasutajaliidese, e-kirjade ja tõuketeadete keel
|
||||
password: Vajalik on vähemalt 8 märki
|
||||
password: Vajalik on vähemalt 8 tähemärki
|
||||
phrase: Kattub olenemata postituse teksti suurtähtedest või sisuhoiatusest
|
||||
scopes: Milliseid API-sid see rakendus tohib kasutada. Kui valid kõrgeima taseme, ei pea üksikuid eraldi valima.
|
||||
setting_advanced_layout: Näita Mastodoni mitme veeruga paigutuses, mispuhul näed korraga nii ajajoont, teavitusi, kui sinu valitud kolmandat veergu. Ei sobi kasutamiseks väikeste ekraanide puhul.
|
||||
@@ -113,7 +113,7 @@ et:
|
||||
trends: Trendid näitavad, millised postitused, teemaviited ja uudislood koguvad sinu serveris tähelepanu.
|
||||
wrapstodon: Paku kohalikele kasutajatele luua nende Mastodoni kasutamise aastast mänguline kokkuvõte. See võimalus on saadaval igal aastal 10. ja 31. detsembri vahel ja seda pakutakse kasutajatele, kes tegid vähemalt ühe avaliku või vaikse avaliku postituse ja kes kasutas aasta jooksul vähemalt ühte silti.
|
||||
form_challenge:
|
||||
current_password: Turvalisse alasse sisenemine
|
||||
current_password: Sisened turvalisse alasse
|
||||
imports:
|
||||
data: CSV fail eksporditi teisest Mastodoni serverist
|
||||
invite_request:
|
||||
@@ -214,8 +214,8 @@ et:
|
||||
avatar: Profiilipilt
|
||||
bot: See konto on robot
|
||||
chosen_languages: Keelte filtreerimine
|
||||
confirm_new_password: Uue salasõna kinnitamine
|
||||
confirm_password: Salasõna kinnitamine
|
||||
confirm_new_password: Korda uut salasõna
|
||||
confirm_password: Korda salasõna
|
||||
context: Filtreeri kontekste
|
||||
current_password: Kehtiv salasõna
|
||||
data: Andmed
|
||||
|
||||
@@ -124,6 +124,8 @@ Rails.application.routes.draw do
|
||||
|
||||
scope path: 'ap', as: 'ap' do
|
||||
resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do
|
||||
resources :featured_collections, only: [:index], module: :activitypub
|
||||
|
||||
resources :statuses, only: [:show] do
|
||||
member do
|
||||
get :activity
|
||||
|
||||
7
db/migrate/20260119153538_add_language_to_collections.rb
Normal file
7
db/migrate/20260119153538_add_language_to_collections.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddLanguageToCollections < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :collections, :language, :string
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_01_15_153219) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2026_01_19_153538) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@@ -382,6 +382,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_15_153219) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "item_count", default: 0, null: false
|
||||
t.string "language"
|
||||
t.index ["account_id"], name: "index_collections_on_account_id"
|
||||
t.index ["tag_id"], name: "index_collections_on_tag_id"
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
web:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.4
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec puma -C config/puma.rb
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
# build:
|
||||
# dockerfile: ./streaming/Dockerfile
|
||||
# context: .
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.4
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming/index.js
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
sidekiq:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.4
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.5
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
||||
@@ -17,7 +17,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
'alpha.2'
|
||||
'alpha.3'
|
||||
end
|
||||
|
||||
def prerelease
|
||||
|
||||
@@ -16,12 +16,18 @@ RSpec.describe Collection do
|
||||
|
||||
it { is_expected.to_not allow_value(nil).for(:discoverable) }
|
||||
|
||||
it { is_expected.to allow_value('en').for(:language) }
|
||||
|
||||
it { is_expected.to_not allow_value('randomstuff').for(:language) }
|
||||
|
||||
context 'when collection is remote' do
|
||||
subject { Fabricate.build :collection, local: false }
|
||||
|
||||
it { is_expected.to validate_presence_of(:uri) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:original_number_of_items) }
|
||||
|
||||
it { is_expected.to allow_value('randomstuff').for(:language) }
|
||||
end
|
||||
|
||||
context 'when using a hashtag as category' do
|
||||
|
||||
@@ -450,6 +450,44 @@ RSpec.describe Account::Interactions do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#blocking_or_domain_blocking?' do
|
||||
subject { account.blocking_or_domain_blocking?(target_account) }
|
||||
|
||||
context 'when blocking target_account' do
|
||||
before do
|
||||
account.block_relationships.create(target_account: target_account)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
result = nil
|
||||
expect { result = subject }.to execute_queries
|
||||
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blocking the domain' do
|
||||
let(:target_account) { Fabricate(:remote_account) }
|
||||
|
||||
before do
|
||||
account_domain_block = Fabricate(:account_domain_block, domain: target_account.domain)
|
||||
account.domain_blocks << account_domain_block
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
result = nil
|
||||
expect { result = subject }.to execute_queries
|
||||
expect(result).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blocking neither target_account nor its domain' do
|
||||
it 'returns false' do
|
||||
expect(subject).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#muting?' do
|
||||
subject { account.muting?(target_account) }
|
||||
|
||||
|
||||
@@ -188,4 +188,24 @@ RSpec.describe AccountPolicy do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :index_collections? do
|
||||
it 'permits when no user is given' do
|
||||
expect(subject).to permit(nil, john)
|
||||
end
|
||||
|
||||
it 'permits unblocked users' do
|
||||
expect(subject).to permit(john, john)
|
||||
expect(subject).to permit(alice, john)
|
||||
end
|
||||
|
||||
it 'denies blocked users' do
|
||||
domain_blocked_user = Fabricate(:remote_account)
|
||||
john.block_domain!(domain_blocked_user.domain)
|
||||
john.block!(alice)
|
||||
|
||||
expect(subject).to_not permit(domain_blocked_user, john)
|
||||
expect(subject).to_not permit(alice, john)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
153
spec/requests/activitypub/featured_collections_spec.rb
Normal file
153
spec/requests/activitypub/featured_collections_spec.rb
Normal file
@@ -0,0 +1,153 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Collections' do
|
||||
describe 'GET /ap/users/@:account_id/featured_collections', feature: :collections do
|
||||
subject { get ap_account_featured_collections_path(account.id, format: :json) }
|
||||
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
let(:account) { collection.account }
|
||||
|
||||
context 'when signed out' do
|
||||
context 'when account is permanently suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily suspended' do
|
||||
before { account.suspend! }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is accessible' do
|
||||
it 'renders ActivityPub Collection successfully', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
|
||||
|
||||
expect(response.headers).to include(
|
||||
'Content-Type' => include('application/activity+json')
|
||||
)
|
||||
expect(response.parsed_body)
|
||||
.to include({
|
||||
'type' => 'Collection',
|
||||
'totalItems' => 1,
|
||||
'first' => match(%r{^https://.*page=1.*$}),
|
||||
})
|
||||
end
|
||||
|
||||
context 'when requesting the first page' do
|
||||
subject { get ap_account_featured_collections_path(account.id, page: 1, format: :json) }
|
||||
|
||||
context 'when account has many collections' do
|
||||
before do
|
||||
Fabricate.times(5, :collection, account:)
|
||||
end
|
||||
|
||||
it 'includes a link to the next page', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to include({
|
||||
'type' => 'CollectionPage',
|
||||
'totalItems' => 6,
|
||||
'next' => match(%r{^https://.*page=2.*$}),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
post user_session_path, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
context 'when account blocks user' do
|
||||
before { account.block!(user.account) }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with "HTTP Signature" access signed by a remote account' do
|
||||
subject do
|
||||
get ap_account_featured_collections_path(account.id, format: :json),
|
||||
headers: nil,
|
||||
sign_with: remote_account
|
||||
end
|
||||
|
||||
let(:remote_account) { Fabricate(:account, domain: 'host.example') }
|
||||
|
||||
context 'when account blocks the remote account' do
|
||||
before { account.block!(remote_account) }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account domain blocks the domain of the remote account' do
|
||||
before { account.block_domain!(remote_account.domain) }
|
||||
|
||||
it 'returns http not found' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with JSON' do
|
||||
it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
|
||||
|
||||
expect(response.headers).to include(
|
||||
'Content-Type' => include('application/activity+json')
|
||||
)
|
||||
expect(response.parsed_body)
|
||||
.to include({
|
||||
'type' => 'Collection',
|
||||
'totalItems' => 1,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -115,6 +115,7 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
|
||||
{
|
||||
name: 'Low-traffic bots',
|
||||
description: 'Really nice bots, please follow',
|
||||
language: 'en',
|
||||
sensitive: '0',
|
||||
discoverable: '1',
|
||||
}
|
||||
|
||||
@@ -163,9 +163,10 @@ RSpec.describe 'API Web Push Subscriptions' do
|
||||
end
|
||||
|
||||
describe 'PUT /api/web/push_subscriptions/:id' do
|
||||
before { sign_in Fabricate :user }
|
||||
before { sign_in user }
|
||||
|
||||
let(:subscription) { Fabricate :web_push_subscription }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:subscription) { Fabricate(:web_push_subscription, user: user) }
|
||||
|
||||
it 'gracefully handles invalid nested params' do
|
||||
put api_web_push_subscription_path(subscription), params: { data: 'invalid' }
|
||||
|
||||
@@ -35,5 +35,11 @@ RSpec.describe ActivityPub::CollectionSerializer do
|
||||
|
||||
it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) }
|
||||
end
|
||||
|
||||
context 'with a Collection' do
|
||||
let(:model) { Collection.new }
|
||||
|
||||
it { is_expected.to eq(ActivityPub::FeaturedCollectionSerializer) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,4 +45,20 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do
|
||||
'updated' => match_api_datetime_format,
|
||||
})
|
||||
end
|
||||
|
||||
context 'when a language is set' do
|
||||
before do
|
||||
collection.language = 'en'
|
||||
end
|
||||
|
||||
it 'uses "summaryMap" to include the language' do
|
||||
expect(subject).to include({
|
||||
'summaryMap' => {
|
||||
'en' => 'These are really amazing',
|
||||
},
|
||||
})
|
||||
|
||||
expect(subject).to_not have_key('summary')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,7 @@ RSpec.describe REST::CollectionSerializer do
|
||||
id: 2342,
|
||||
name: 'Exquisite follows',
|
||||
description: 'Always worth a follow',
|
||||
language: 'en',
|
||||
local: true,
|
||||
sensitive: true,
|
||||
discoverable: false,
|
||||
@@ -31,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do
|
||||
'id' => '2342',
|
||||
'name' => 'Exquisite follows',
|
||||
'description' => 'Always worth a follow',
|
||||
'language' => 'en',
|
||||
'local' => true,
|
||||
'sensitive' => true,
|
||||
'discoverable' => false,
|
||||
|
||||
Reference in New Issue
Block a user