Merge pull request #3360 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 400c1f3e8ec0ffd33ad30d9334b9210cdb89b14c
This commit is contained in:
commit
e612378f08
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [4.5.4] - 2026-01-07
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
|
|||||||
### Additional documentation
|
### Additional documentation
|
||||||
|
|
||||||
- [Mastodon documentation](https://docs.joinmastodon.org/)
|
- [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
|
class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
|
||||||
|
before_action :skip_large_payload
|
||||||
before_action :skip_unknown_actor_activity
|
before_action :skip_unknown_actor_activity
|
||||||
before_action :require_actor_signature!
|
before_action :require_actor_signature!
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def skip_large_payload
|
||||||
|
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
|
||||||
|
end
|
||||||
|
|
||||||
def skip_unknown_actor_activity
|
def skip_unknown_actor_activity
|
||||||
head 202 if unknown_affected_account?
|
head 202 if unknown_affected_account?
|
||||||
end
|
end
|
||||||
|
|||||||
@ -81,11 +81,11 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def collection_creation_params
|
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
|
end
|
||||||
|
|
||||||
def collection_update_params
|
def collection_update_params
|
||||||
params.permit(:name, :description, :sensitive, :discoverable, :tag_name)
|
params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_feature_enabled
|
def check_feature_enabled
|
||||||
|
|||||||
@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_push_subscription
|
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
|
end
|
||||||
|
|
||||||
def subscription_params
|
def subscription_params
|
||||||
|
|||||||
@ -107,15 +107,18 @@ export default function api(withAuthorization = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`;
|
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,
|
method: Method,
|
||||||
url: string,
|
url: string,
|
||||||
args: {
|
args: {
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
params?: RequestParamsOrData;
|
params?: RequestParamsOrData<ApiParamsOrData>;
|
||||||
data?: RequestParamsOrData;
|
data?: RequestParamsOrData<ApiParamsOrData>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
@ -128,30 +131,30 @@ export async function apiRequest<ApiResponse = unknown>(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestGet<ApiResponse = unknown>(
|
export async function apiRequestGet<ApiResponse = unknown, ApiParams = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
params?: RequestParamsOrData,
|
params?: RequestParamsOrData<ApiParams>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('GET', url, { params });
|
return apiRequest<ApiResponse>('GET', url, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestPost<ApiResponse = unknown>(
|
export async function apiRequestPost<ApiResponse = unknown, ApiData = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
data?: RequestParamsOrData,
|
data?: RequestParamsOrData<ApiData>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('POST', url, { data });
|
return apiRequest<ApiResponse>('POST', url, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestPut<ApiResponse = unknown>(
|
export async function apiRequestPut<ApiResponse = unknown, ApiData = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
data?: RequestParamsOrData,
|
data?: RequestParamsOrData<ApiData>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('PUT', url, { data });
|
return apiRequest<ApiResponse>('PUT', url, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestDelete<ApiResponse = unknown>(
|
export async function apiRequestDelete<
|
||||||
url: ApiUrl,
|
ApiResponse = unknown,
|
||||||
params?: RequestParamsOrData,
|
ApiParams = unknown,
|
||||||
) {
|
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
||||||
return apiRequest<ApiResponse>('DELETE', url, { params });
|
return apiRequest<ApiResponse>('DELETE', url, { params });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
import { useState, useRef, useCallback, useId } from 'react';
|
import { useState, useRef, useCallback, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
@ -15,7 +16,9 @@ export const DomainPill: React.FC<{
|
|||||||
domain: string;
|
domain: string;
|
||||||
username: string;
|
username: string;
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
}> = ({ domain, username, isSelf }) => {
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ domain, username, isSelf, children, className }) => {
|
||||||
const accessibilityId = useId();
|
const accessibilityId = useId();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
@ -32,7 +35,9 @@ export const DomainPill: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={classNames('account__domain-pill', { active: open })}
|
className={classNames('account__domain-pill', className, {
|
||||||
|
active: open,
|
||||||
|
})}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@ -40,6 +45,7 @@ export const DomainPill: React.FC<{
|
|||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
{domain}
|
{domain}
|
||||||
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Overlay
|
<Overlay
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { AccountBio } from '@/flavours/glitch/components/account_bio';
|
import { AccountBio } from '@/flavours/glitch/components/account_bio';
|
||||||
import { DisplayName } from '@/flavours/glitch/components/display_name';
|
|
||||||
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
|
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
|
||||||
import { DomainPill } from 'flavours/glitch/features/account/components/domain_pill';
|
|
||||||
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
|
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
|
||||||
import {
|
import {
|
||||||
autoPlayGif,
|
autoPlayGif,
|
||||||
@ -25,7 +19,9 @@ import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
|||||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { ActionBar } from '../../account/components/action_bar';
|
import { ActionBar } from '../../account/components/action_bar';
|
||||||
|
import { isRedesignEnabled } from '../common';
|
||||||
|
|
||||||
|
import { AccountName } from './account_name';
|
||||||
import { AccountBadges } from './badges';
|
import { AccountBadges } from './badges';
|
||||||
import { AccountButtons } from './buttons';
|
import { AccountButtons } from './buttons';
|
||||||
import { FamiliarFollowers } from './familiar_followers';
|
import { FamiliarFollowers } from './familiar_followers';
|
||||||
@ -33,6 +29,7 @@ import { AccountHeaderFields } from './fields';
|
|||||||
import { AccountInfo } from './info';
|
import { AccountInfo } from './info';
|
||||||
import { MemorialNote } from './memorial_note';
|
import { MemorialNote } from './memorial_note';
|
||||||
import { MovedNote } from './moved_note';
|
import { MovedNote } from './moved_note';
|
||||||
|
import redesignClasses from './redesign.module.scss';
|
||||||
import { AccountTabs } from './tabs';
|
import { AccountTabs } from './tabs';
|
||||||
|
|
||||||
const titleFromAccount = (account: Account) => {
|
const titleFromAccount = (account: Account) => {
|
||||||
@ -52,7 +49,6 @@ export const AccountHeader: React.FC<{
|
|||||||
hideTabs?: boolean;
|
hideTabs?: boolean;
|
||||||
}> = ({ accountId, hideTabs }) => {
|
}> = ({ accountId, hideTabs }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
const relationship = useAppSelector((state) =>
|
const relationship = useAppSelector((state) =>
|
||||||
state.relationships.get(accountId),
|
state.relationships.get(accountId),
|
||||||
@ -90,8 +86,6 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
const suspendedOrHidden = hidden || account.suspended;
|
const suspendedOrHidden = hidden || account.suspended;
|
||||||
const isLocal = !account.acct.includes('@');
|
const isLocal = !account.acct.includes('@');
|
||||||
const username = account.acct.split('@')[0];
|
|
||||||
const domain = isLocal ? localDomain : account.acct.split('@')[1];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
@ -138,38 +132,27 @@ export const AccountHeader: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<AccountButtons
|
{!isRedesignEnabled() && (
|
||||||
accountId={accountId}
|
<AccountButtons
|
||||||
className='account__header__buttons--desktop'
|
accountId={accountId}
|
||||||
/>
|
className='account__header__buttons--desktop'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div
|
||||||
<h1>
|
className={classNames(
|
||||||
<DisplayName account={account} variant='simple' />
|
'account__header__tabs__name',
|
||||||
<small>
|
isRedesignEnabled() && redesignClasses.nameWrapper,
|
||||||
<span>
|
)}
|
||||||
@{username}
|
>
|
||||||
<span className='invisible'>@{domain}</span>
|
<AccountName
|
||||||
</span>
|
accountId={accountId}
|
||||||
<DomainPill
|
className={classNames(
|
||||||
username={username ?? ''}
|
isRedesignEnabled() && redesignClasses.name,
|
||||||
domain={domain ?? ''}
|
)}
|
||||||
isSelf={me === account.id}
|
/>
|
||||||
/>
|
{isRedesignEnabled() && <AccountButtons accountId={accountId} />}
|
||||||
{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>
|
</div>
|
||||||
|
|
||||||
<AccountBadges accountId={accountId} />
|
<AccountBadges accountId={accountId} />
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { DisplayName } from '@/flavours/glitch/components/display_name';
|
||||||
|
import { Icon } from '@/flavours/glitch/components/icon';
|
||||||
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
|
import { useAppSelector } from '@/flavours/glitch/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 {
|
.fieldList {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,15 +128,18 @@ export default function api(withAuthorization = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`;
|
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,
|
method: Method,
|
||||||
url: string,
|
url: string,
|
||||||
args: {
|
args: {
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
params?: RequestParamsOrData;
|
params?: RequestParamsOrData<ApiParamsOrData>;
|
||||||
data?: RequestParamsOrData;
|
data?: RequestParamsOrData<ApiParamsOrData>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
@ -149,30 +152,30 @@ export async function apiRequest<ApiResponse = unknown>(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestGet<ApiResponse = unknown>(
|
export async function apiRequestGet<ApiResponse = unknown, ApiParams = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
params?: RequestParamsOrData,
|
params?: RequestParamsOrData<ApiParams>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('GET', url, { params });
|
return apiRequest<ApiResponse>('GET', url, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestPost<ApiResponse = unknown>(
|
export async function apiRequestPost<ApiResponse = unknown, ApiData = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
data?: RequestParamsOrData,
|
data?: RequestParamsOrData<ApiData>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('POST', url, { data });
|
return apiRequest<ApiResponse>('POST', url, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestPut<ApiResponse = unknown>(
|
export async function apiRequestPut<ApiResponse = unknown, ApiData = unknown>(
|
||||||
url: ApiUrl,
|
url: ApiUrl,
|
||||||
data?: RequestParamsOrData,
|
data?: RequestParamsOrData<ApiData>,
|
||||||
) {
|
) {
|
||||||
return apiRequest<ApiResponse>('PUT', url, { data });
|
return apiRequest<ApiResponse>('PUT', url, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequestDelete<ApiResponse = unknown>(
|
export async function apiRequestDelete<
|
||||||
url: ApiUrl,
|
ApiResponse = unknown,
|
||||||
params?: RequestParamsOrData,
|
ApiParams = unknown,
|
||||||
) {
|
>(url: ApiUrl, params?: RequestParamsOrData<ApiParams>) {
|
||||||
return apiRequest<ApiResponse>('DELETE', url, { params });
|
return apiRequest<ApiResponse>('DELETE', url, { params });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
import { useState, useRef, useCallback, useId } from 'react';
|
import { useState, useRef, useCallback, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
@ -15,7 +16,9 @@ export const DomainPill: React.FC<{
|
|||||||
domain: string;
|
domain: string;
|
||||||
username: string;
|
username: string;
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
}> = ({ domain, username, isSelf }) => {
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ domain, username, isSelf, children, className }) => {
|
||||||
const accessibilityId = useId();
|
const accessibilityId = useId();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
@ -32,7 +35,9 @@ export const DomainPill: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={classNames('account__domain-pill', { active: open })}
|
className={classNames('account__domain-pill', className, {
|
||||||
|
active: open,
|
||||||
|
})}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@ -40,6 +45,7 @@ export const DomainPill: React.FC<{
|
|||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
{domain}
|
{domain}
|
||||||
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Overlay
|
<Overlay
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||||
import { DisplayName } from '@/mastodon/components/display_name';
|
|
||||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
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 FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
||||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { isRedesignEnabled } from '../common';
|
||||||
|
|
||||||
|
import { AccountName } from './account_name';
|
||||||
import { AccountBadges } from './badges';
|
import { AccountBadges } from './badges';
|
||||||
import { AccountButtons } from './buttons';
|
import { AccountButtons } from './buttons';
|
||||||
import { FamiliarFollowers } from './familiar_followers';
|
import { FamiliarFollowers } from './familiar_followers';
|
||||||
@ -28,6 +25,7 @@ import { AccountInfo } from './info';
|
|||||||
import { MemorialNote } from './memorial_note';
|
import { MemorialNote } from './memorial_note';
|
||||||
import { MovedNote } from './moved_note';
|
import { MovedNote } from './moved_note';
|
||||||
import { AccountNumberFields } from './number_fields';
|
import { AccountNumberFields } from './number_fields';
|
||||||
|
import redesignClasses from './redesign.module.scss';
|
||||||
import { AccountTabs } from './tabs';
|
import { AccountTabs } from './tabs';
|
||||||
|
|
||||||
const titleFromAccount = (account: Account) => {
|
const titleFromAccount = (account: Account) => {
|
||||||
@ -47,7 +45,6 @@ export const AccountHeader: React.FC<{
|
|||||||
hideTabs?: boolean;
|
hideTabs?: boolean;
|
||||||
}> = ({ accountId, hideTabs }) => {
|
}> = ({ accountId, hideTabs }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
const relationship = useAppSelector((state) =>
|
const relationship = useAppSelector((state) =>
|
||||||
state.relationships.get(accountId),
|
state.relationships.get(accountId),
|
||||||
@ -85,8 +82,6 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
const suspendedOrHidden = hidden || account.suspended;
|
const suspendedOrHidden = hidden || account.suspended;
|
||||||
const isLocal = !account.acct.includes('@');
|
const isLocal = !account.acct.includes('@');
|
||||||
const username = account.acct.split('@')[0];
|
|
||||||
const domain = isLocal ? localDomain : account.acct.split('@')[1];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
@ -133,38 +128,27 @@ export const AccountHeader: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<AccountButtons
|
{!isRedesignEnabled() && (
|
||||||
accountId={accountId}
|
<AccountButtons
|
||||||
className='account__header__buttons--desktop'
|
accountId={accountId}
|
||||||
/>
|
className='account__header__buttons--desktop'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div
|
||||||
<h1>
|
className={classNames(
|
||||||
<DisplayName account={account} variant='simple' />
|
'account__header__tabs__name',
|
||||||
<small>
|
isRedesignEnabled() && redesignClasses.nameWrapper,
|
||||||
<span>
|
)}
|
||||||
@{username}
|
>
|
||||||
<span className='invisible'>@{domain}</span>
|
<AccountName
|
||||||
</span>
|
accountId={accountId}
|
||||||
<DomainPill
|
className={classNames(
|
||||||
username={username ?? ''}
|
isRedesignEnabled() && redesignClasses.name,
|
||||||
domain={domain ?? ''}
|
)}
|
||||||
isSelf={me === account.id}
|
/>
|
||||||
/>
|
{isRedesignEnabled() && <AccountButtons accountId={accountId} />}
|
||||||
{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>
|
</div>
|
||||||
|
|
||||||
<AccountBadges accountId={accountId} />
|
<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 {
|
.fieldList {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
"about.blocks": "Modereeritavad serverid",
|
"about.blocks": "Modereeritavad serverid",
|
||||||
"about.contact": "Kontakt:",
|
"about.contact": "Kontakt:",
|
||||||
"about.default_locale": "Vaikimisi",
|
"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.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.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 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.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.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.domain_blocks.suspended.title": "Peatatud",
|
||||||
"about.language_label": "Keel",
|
"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.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}",
|
||||||
"about.rules": "Serveri reeglid",
|
"about.rules": "Serveri reeglid",
|
||||||
"account.account_note_header": "Isiklik märge",
|
"account.account_note_header": "Isiklik märge",
|
||||||
@ -18,19 +18,19 @@
|
|||||||
"account.badges.bot": "Robot",
|
"account.badges.bot": "Robot",
|
||||||
"account.badges.group": "Grupp",
|
"account.badges.group": "Grupp",
|
||||||
"account.block": "Blokeeri @{name}",
|
"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.block_short": "Blokeerimine",
|
||||||
"account.blocked": "Blokeeritud",
|
"account.blocked": "Blokeeritud",
|
||||||
"account.blocking": "Blokeeritud kasutaja",
|
"account.blocking": "Blokeeritud kasutaja",
|
||||||
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
|
||||||
"account.copy": "Kopeeri profiili link",
|
"account.copy": "Kopeeri profiili link",
|
||||||
"account.direct": "Maini privaatselt @{name}",
|
"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.domain_blocking": "Blokeeritud domeen",
|
||||||
"account.edit_profile": "Muuda profiili",
|
"account.edit_profile": "Muuda profiili",
|
||||||
"account.edit_profile_short": "Muuda",
|
"account.edit_profile_short": "Muuda",
|
||||||
"account.enable_notifications": "Teavita mind @{name} postitustest",
|
"account.enable_notifications": "Teavita mind, kui {name} postitab",
|
||||||
"account.endorse": "Too profiilil esile",
|
"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_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_one": "Jälgijaks {name1}",
|
||||||
"account.familiar_followers_two": "Jälgijateks {name1} ja {name2}",
|
"account.familiar_followers_two": "Jälgijateks {name1} ja {name2}",
|
||||||
@ -57,11 +57,12 @@
|
|||||||
"account.go_to_profile": "Mine profiilile",
|
"account.go_to_profile": "Mine profiilile",
|
||||||
"account.hide_reblogs": "Peida @{name} jagamised",
|
"account.hide_reblogs": "Peida @{name} jagamised",
|
||||||
"account.in_memoriam": "In Memoriam.",
|
"account.in_memoriam": "In Memoriam.",
|
||||||
|
"account.joined_long": "Liitus {date}",
|
||||||
"account.joined_short": "Liitus",
|
"account.joined_short": "Liitus",
|
||||||
"account.languages": "Muuda tellitud keeli",
|
"account.languages": "Muuda tellitud keeli",
|
||||||
"account.link_verified_on": "Selle lingi autorsust kontrolliti {date}",
|
"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.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.mention": "Maini @{name}",
|
||||||
"account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:",
|
"account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:",
|
||||||
"account.mute": "Summuta @{name}",
|
"account.mute": "Summuta @{name}",
|
||||||
@ -81,20 +82,22 @@
|
|||||||
"account.share": "Jaga @{name} profiili",
|
"account.share": "Jaga @{name} profiili",
|
||||||
"account.show_reblogs": "Näita @{name} jagamisi",
|
"account.show_reblogs": "Näita @{name} jagamisi",
|
||||||
"account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}",
|
"account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}",
|
||||||
"account.unblock": "Eemalda blokeering @{name}",
|
"account.unblock": "Lõpeta {name} kasutaja blokeerimine",
|
||||||
"account.unblock_domain": "Tee {domain} nähtavaks",
|
"account.unblock_domain": "Lõpeta {domain} domeeni blokeerimine",
|
||||||
"account.unblock_domain_short": "Lõpeta 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.unendorse": "Ära kuva profiilil",
|
||||||
"account.unfollow": "Jälgid",
|
"account.unfollow": "Ära jälgi",
|
||||||
"account.unmute": "Lõpeta {name} kasutaja summutamine",
|
"account.unmute": "Lõpeta {name} kasutaja summutamine",
|
||||||
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
|
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
|
||||||
"account.unmute_short": "Lõpeta 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",
|
"account_note.placeholder": "Klõpsa märke lisamiseks",
|
||||||
"admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist",
|
"admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist",
|
||||||
"admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist",
|
"admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist",
|
||||||
"admin.dashboard.retention.average": "Keskmine",
|
"admin.dashboard.retention.average": "Keskmine",
|
||||||
"admin.dashboard.retention.cohort": "Registreerumiskuu",
|
"admin.dashboard.retention.cohort": "Liitumiskuu",
|
||||||
"admin.dashboard.retention.cohort_size": "Uued kasutajad",
|
"admin.dashboard.retention.cohort_size": "Uued kasutajad",
|
||||||
"admin.impact_report.instance_accounts": "Kontode profiilid, mille see kustutaks",
|
"admin.impact_report.instance_accounts": "Kontode profiilid, mille see kustutaks",
|
||||||
"admin.impact_report.instance_followers": "Jälgijad, kelle meie kasutajad kaotaks",
|
"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.message": "Palun proovi uuesti pärast {retry_time, time, medium}.",
|
||||||
"alert.rate_limited.title": "Kiiruspiirang",
|
"alert.rate_limited.title": "Kiiruspiirang",
|
||||||
"alert.unexpected.message": "Tekkis ootamatu viga.",
|
"alert.unexpected.message": "Tekkis ootamatu viga.",
|
||||||
"alert.unexpected.title": "Oih!",
|
"alert.unexpected.title": "Vaat kus lops!",
|
||||||
"alt_text_badge.title": "Alternatiivtekst",
|
"alt_text_badge.title": "Selgitustekst",
|
||||||
"alt_text_modal.add_alt_text": "Lisa alt-tekst",
|
"alt_text_modal.add_alt_text": "Lisa selgitustekst",
|
||||||
"alt_text_modal.add_text_from_image": "Lisa tekst pildilt",
|
"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.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_hearing_impairments": "Kirjelda seda kuulmispuudega inimeste jaoks…",
|
||||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Kirjelda seda nägemispuudega 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.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.",
|
||||||
"annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis",
|
"annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis",
|
||||||
"annual_report.nav_item.badge": "Uus",
|
"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": "Loodud {heart} Mastodoni meeskonna poolt",
|
||||||
"annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.",
|
"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.",
|
"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}}",
|
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
||||||
"loading_indicator.label": "Laadimine…",
|
"loading_indicator.label": "Laadimine…",
|
||||||
"media_gallery.hide": "Peida",
|
"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}.",
|
"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_from_notifications": "Peida teavituste hulgast",
|
||||||
"mute_modal.hide_options": "Peida valikud",
|
"mute_modal.hide_options": "Peida valikud",
|
||||||
@ -767,7 +771,7 @@
|
|||||||
"onboarding.profile.upload_avatar": "Laadi üles profiilipilt",
|
"onboarding.profile.upload_avatar": "Laadi üles profiilipilt",
|
||||||
"onboarding.profile.upload_header": "Laadi üles profiili päis",
|
"onboarding.profile.upload_header": "Laadi üles profiili päis",
|
||||||
"password_confirmation.exceeds_maxlength": "Salasõnakinnitus on pikem kui salasõna maksimumpikkus",
|
"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",
|
"picture_in_picture.restore": "Pane tagasi",
|
||||||
"poll.closed": "Suletud",
|
"poll.closed": "Suletud",
|
||||||
"poll.refresh": "Värskenda",
|
"poll.refresh": "Värskenda",
|
||||||
|
|||||||
@ -57,6 +57,7 @@
|
|||||||
"account.go_to_profile": "Profile git",
|
"account.go_to_profile": "Profile git",
|
||||||
"account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle",
|
"account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle",
|
||||||
"account.in_memoriam": "Hatırasına.",
|
"account.in_memoriam": "Hatırasına.",
|
||||||
|
"account.joined_long": "{date} tarihinde katıldı",
|
||||||
"account.joined_short": "Katıldı",
|
"account.joined_short": "Katıldı",
|
||||||
"account.languages": "Abone olunan dilleri değiştir",
|
"account.languages": "Abone olunan dilleri değiştir",
|
||||||
"account.link_verified_on": "Bu bağlantının sahipliği {date} tarihinde denetlendi",
|
"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": "@{name} adlı kişinin sesini aç",
|
||||||
"account.unmute_notifications_short": "Bildirimlerin sesini aç",
|
"account.unmute_notifications_short": "Bildirimlerin sesini aç",
|
||||||
"account.unmute_short": "Susturmayı kaldır",
|
"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",
|
"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.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ı",
|
"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}}",
|
"load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}",
|
||||||
"loading_indicator.label": "Yükleniyor…",
|
"loading_indicator.label": "Yükleniyor…",
|
||||||
"media_gallery.hide": "Gizle",
|
"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ışı.",
|
"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_from_notifications": "Bildirimlerde gizle",
|
||||||
"mute_modal.hide_options": "Seçenekleri gizle",
|
"mute_modal.hide_options": "Seçenekleri gizle",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ class ActivityPub::Activity
|
|||||||
include Redisable
|
include Redisable
|
||||||
include Lockable
|
include Lockable
|
||||||
|
|
||||||
|
MAX_JSON_SIZE = 1.megabyte
|
||||||
SUPPORTED_TYPES = %w(Note Question).freeze
|
SUPPORTED_TYPES = %w(Note Question).freeze
|
||||||
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).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)
|
@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
|
# 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
|
# We may be getting `Create` and `Update` out of order
|
||||||
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
class ActivityPub::Parser::PollParser
|
class ActivityPub::Parser::PollParser
|
||||||
include JsonLdHelper
|
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)
|
def initialize(json)
|
||||||
@json = json
|
@json = json
|
||||||
end
|
end
|
||||||
@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser
|
|||||||
private
|
private
|
||||||
|
|
||||||
def items
|
def items
|
||||||
@json['anyOf'] || @json['oneOf']
|
(@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -81,6 +81,13 @@ class Account < ApplicationRecord
|
|||||||
DISPLAY_NAME_LENGTH_LIMIT = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
DISPLAY_NAME_LENGTH_LIMIT = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
||||||
NOTE_LENGTH_LIMIT = (ENV['MAX_BIO_CHARS'] || 500).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
|
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
|
||||||
|
|
||||||
include Attachmentable # Load prior to Avatar & Header concerns
|
include Attachmentable # Load prior to Avatar & Header concerns
|
||||||
@ -114,7 +121,7 @@ class Account < ApplicationRecord
|
|||||||
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
|
||||||
|
|
||||||
# Remote user validations, also applies to internal actors
|
# 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
|
# Remote user validations
|
||||||
validates :uri, presence: true, unless: :local?, on: :create
|
validates :uri, presence: true, unless: :local?, on: :create
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
# description :text not null
|
# description :text not null
|
||||||
# discoverable :boolean not null
|
# discoverable :boolean not null
|
||||||
# item_count :integer default(0), not null
|
# item_count :integer default(0), not null
|
||||||
|
# language :string
|
||||||
# local :boolean not null
|
# local :boolean not null
|
||||||
# name :string not null
|
# name :string not null
|
||||||
# original_number_of_items :integer
|
# original_number_of_items :integer
|
||||||
@ -36,6 +37,7 @@ class Collection < ApplicationRecord
|
|||||||
presence: true,
|
presence: true,
|
||||||
numericality: { greater_than_or_equal: 0 },
|
numericality: { greater_than_or_equal: 0 },
|
||||||
if: :remote?
|
if: :remote?
|
||||||
|
validates :language, language: { if: :local?, allow_nil: true }
|
||||||
validate :tag_is_usable
|
validate :tag_is_usable
|
||||||
validate :items_do_not_exceed_limit
|
validate :items_do_not_exceed_limit
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,13 @@ module Account::Interactions
|
|||||||
end
|
end
|
||||||
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)
|
def muting?(other_account)
|
||||||
other_id = other_account.is_a?(Account) ? other_account.id : 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
|
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
|
||||||
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
|
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
|
||||||
MINIMUM_SHORTCODE_SIZE = 2
|
MINIMUM_SHORTCODE_SIZE = 2
|
||||||
|
MAX_SHORTCODE_SIZE = 128
|
||||||
|
MAX_FEDERATED_SHORTCODE_SIZE = 2048
|
||||||
|
|
||||||
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
|
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 :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: LIMIT, unless: :local?
|
||||||
validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :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 :local, -> { where(domain: nil) }
|
||||||
scope :remote, -> { where.not(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
|
EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze
|
||||||
|
|
||||||
|
TITLE_LENGTH_LIMIT = 256
|
||||||
|
|
||||||
include Expireable
|
include Expireable
|
||||||
include Redisable
|
include Redisable
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord
|
|||||||
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
||||||
|
|
||||||
validates :title, :context, presence: true
|
validates :title, :context, presence: true
|
||||||
|
validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
|
||||||
validate :context_must_be_valid
|
validate :context_must_be_valid
|
||||||
|
|
||||||
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }
|
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }
|
||||||
|
|||||||
@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :custom_filter
|
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
|
alias_attribute :phrase, :keyword
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class List < ApplicationRecord
|
|||||||
include Paginable
|
include Paginable
|
||||||
|
|
||||||
PER_ACCOUNT_LIMIT = 50
|
PER_ACCOUNT_LIMIT = 50
|
||||||
|
TITLE_LENGTH_LIMIT = 256
|
||||||
|
|
||||||
enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true
|
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 :accounts, through: :list_accounts
|
||||||
has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account
|
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
|
validate :validate_account_lists_limit, on: :create
|
||||||
|
|
||||||
|
|||||||
@ -68,4 +68,8 @@ class AccountPolicy < ApplicationPolicy
|
|||||||
def feature?
|
def feature?
|
||||||
record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record)
|
record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def index_collections?
|
||||||
|
current_account.nil? || !record.blocking_or_domain_blocking?(current_account)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show?
|
def show?
|
||||||
current_account.nil? || (!owner_blocking? && !owner_blocking_domain?)
|
current_account.nil? || !owner.blocking_or_domain_blocking?(current_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create?
|
def create?
|
||||||
@ -27,18 +27,6 @@ class CollectionPolicy < ApplicationPolicy
|
|||||||
current_account == owner
|
current_account == owner
|
||||||
end
|
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
|
def owner
|
||||||
record.account
|
record.account
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||||||
:discoverable, :indexable, :published, :memorial
|
:discoverable, :indexable, :published, :memorial
|
||||||
|
|
||||||
attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? }
|
attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? }
|
||||||
|
attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? }
|
||||||
|
|
||||||
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
|
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
|
||||||
|
|
||||||
@ -177,6 +178,12 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def featured_collections
|
||||||
|
return nil if instance_actor?
|
||||||
|
|
||||||
|
ap_account_featured_collections_url(object.id)
|
||||||
|
end
|
||||||
|
|
||||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
|
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer
|
|||||||
ActivityPub::HashtagSerializer
|
ActivityPub::HashtagSerializer
|
||||||
when 'ActivityPub::CollectionPresenter'
|
when 'ActivityPub::CollectionPresenter'
|
||||||
ActivityPub::CollectionSerializer
|
ActivityPub::CollectionSerializer
|
||||||
|
when 'Collection'
|
||||||
|
ActivityPub::FeaturedCollectionSerializer
|
||||||
when 'String'
|
when 'String'
|
||||||
StringSerializer
|
StringSerializer
|
||||||
else
|
else
|
||||||
|
|||||||
@ -17,9 +17,12 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes :id, :type, :total_items, :name, :summary, :attributed_to,
|
attributes :id, :type, :total_items, :name, :attributed_to,
|
||||||
:sensitive, :discoverable, :published, :updated
|
: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_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer
|
||||||
|
|
||||||
has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer
|
has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer
|
||||||
@ -36,6 +39,10 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
|||||||
object.description
|
object.description
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def summary_map
|
||||||
|
{ object.language => object.description }
|
||||||
|
end
|
||||||
|
|
||||||
def attributed_to
|
def attributed_to
|
||||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||||
end
|
end
|
||||||
@ -51,4 +58,8 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
|||||||
def updated
|
def updated
|
||||||
object.updated_at.iso8601
|
object.updated_at.iso8601
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def language_present?
|
||||||
|
object.language.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::BaseCollectionSerializer < ActiveModel::Serializer
|
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
|
:discoverable, :item_count, :created_at, :updated_at
|
||||||
|
|
||||||
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
|
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
|
||||||
|
|||||||
@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
include Redisable
|
include Redisable
|
||||||
include Lockable
|
include Lockable
|
||||||
|
|
||||||
|
MAX_PROFILE_FIELDS = 50
|
||||||
SUBDOMAINS_RATELIMIT = 10
|
SUBDOMAINS_RATELIMIT = 10
|
||||||
DISCOVERIES_PER_REQUEST = 400
|
DISCOVERIES_PER_REQUEST = 400
|
||||||
|
|
||||||
@ -124,15 +125,15 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
|
|
||||||
def set_immediate_attributes!
|
def set_immediate_attributes!
|
||||||
@account.featured_collection_url = valid_collection_uri(@json['featured'])
|
@account.featured_collection_url = valid_collection_uri(@json['featured'])
|
||||||
@account.display_name = @json['name'] || ''
|
@account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)]
|
||||||
@account.note = @json['summary'] || ''
|
@account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)]
|
||||||
@account.locked = @json['manuallyApprovesFollowers'] || false
|
@account.locked = @json['manuallyApprovesFollowers'] || false
|
||||||
@account.fields = property_values || {}
|
@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.discoverable = @json['discoverable'] || false
|
||||||
@account.indexable = @json['indexable'] || false
|
@account.indexable = @json['indexable'] || false
|
||||||
@account.memorial = @json['memorial'] || 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
|
end
|
||||||
|
|
||||||
def set_fetchable_key!
|
def set_fetchable_key!
|
||||||
@ -253,7 +254,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||||||
def property_values
|
def property_values
|
||||||
return unless @json['attachment'].is_a?(Array)
|
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
|
end
|
||||||
|
|
||||||
def mismatching_origin?(url)
|
def mismatching_origin?(url)
|
||||||
|
|||||||
@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService
|
|||||||
@account = status.account
|
@account = status.account
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
return if @status.proper.account.suspended?
|
||||||
|
|
||||||
check_race_condition!
|
check_race_condition!
|
||||||
warm_payload_cache!
|
warm_payload_cache!
|
||||||
|
|
||||||
|
|||||||
@ -29,23 +29,23 @@ et:
|
|||||||
title: Kinnita e-postiaadress
|
title: Kinnita e-postiaadress
|
||||||
email_changed:
|
email_changed:
|
||||||
explanation: 'Sinu konto e-postiaadress muudetakse:'
|
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'
|
subject: 'Mastodon: e-post muudetud'
|
||||||
title: Uus e-postiaadress
|
title: Uus e-postiaadress
|
||||||
password_change:
|
password_change:
|
||||||
explanation: Konto salasõna on vahetatud.
|
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.
|
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'
|
subject: 'Mastodon: salasõna on muudetud'
|
||||||
title: Salasõna muudetud
|
title: Salasõna on muudetud
|
||||||
reconfirmation_instructions:
|
reconfirmation_instructions:
|
||||||
explanation: Kinnita uus aadress, et oma e-posti aadress muuta.
|
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.
|
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'
|
subject: 'Mastodon: kinnita e-postiaadress %{instance} jaoks'
|
||||||
title: Kinnita e-postiaadress
|
title: Kinnita e-postiaadress
|
||||||
reset_password_instructions:
|
reset_password_instructions:
|
||||||
action: Salasõna muutmine
|
action: Muuda salasõna
|
||||||
explanation: Kontole on küsitud uut salasõna.
|
explanation: Sa palusid oma kasutajakontole luua uus 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.
|
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'
|
subject: 'Mastodon: salasõna lähtestamisjuhendid'
|
||||||
title: Salasõna lähtestamine
|
title: Salasõna lähtestamine
|
||||||
two_factor_disabled:
|
two_factor_disabled:
|
||||||
|
|||||||
@ -142,7 +142,7 @@ et:
|
|||||||
security: Turvalisus
|
security: Turvalisus
|
||||||
security_measures:
|
security_measures:
|
||||||
only_password: Ainult salasõna
|
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
|
sensitive: Tundlik sisu
|
||||||
sensitized: Märgitud kui tundlik sisu
|
sensitized: Märgitud kui tundlik sisu
|
||||||
shared_inbox_url: Jagatud sisendkausta URL
|
shared_inbox_url: Jagatud sisendkausta URL
|
||||||
@ -292,7 +292,7 @@ et:
|
|||||||
remove_avatar_user_html: "%{name} eemaldas %{target} avatari"
|
remove_avatar_user_html: "%{name} eemaldas %{target} avatari"
|
||||||
reopen_report_html: "%{name} taasavas raporti %{target}"
|
reopen_report_html: "%{name} taasavas raporti %{target}"
|
||||||
resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti"
|
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}"
|
resolve_report_html: "%{name} lahendas raporti %{target}"
|
||||||
sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu"
|
sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu"
|
||||||
silence_account_html: "%{name} piiras %{target} konto"
|
silence_account_html: "%{name} piiras %{target} konto"
|
||||||
@ -787,7 +787,7 @@ et:
|
|||||||
manage_taxonomies: Halda taksonoomiaid
|
manage_taxonomies: Halda taksonoomiaid
|
||||||
manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada teemaviidete seadistusi
|
manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada teemaviidete seadistusi
|
||||||
manage_user_access: Halda kasutajate ligipääsu
|
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: Kasutajate haldamine
|
||||||
manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid
|
manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid
|
||||||
manage_webhooks: Halda webhook'e
|
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!
|
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?
|
didnt_get_confirmation: Ei saanud kinnituslinki?
|
||||||
dont_have_your_security_key: Pole turvavõtit?
|
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.
|
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_otp: Kaheastmeline kood telefonist või taastekood
|
||||||
link_to_webauth: Turvavõtmete seadme kasutamine
|
link_to_webauth: Turvavõtmete seadme kasutamine
|
||||||
@ -1270,7 +1270,7 @@ et:
|
|||||||
register: Loo konto
|
register: Loo konto
|
||||||
registration_closed: "%{instance} ei võta vastu uusi liikmeid"
|
registration_closed: "%{instance} ei võta vastu uusi liikmeid"
|
||||||
resend_confirmation: Saada kinnituslink uuesti
|
resend_confirmation: Saada kinnituslink uuesti
|
||||||
reset_password: Salasõna lähtestamine
|
reset_password: Lähtesta salasõna
|
||||||
rules:
|
rules:
|
||||||
accept: Nõus
|
accept: Nõus
|
||||||
back: Tagasi
|
back: Tagasi
|
||||||
@ -1280,7 +1280,7 @@ et:
|
|||||||
title: Mõned põhireeglid.
|
title: Mõned põhireeglid.
|
||||||
title_invited: Oled kutsutud.
|
title_invited: Oled kutsutud.
|
||||||
security: Turvalisus
|
security: Turvalisus
|
||||||
set_new_password: Uue salasõna määramine
|
set_new_password: Sisesta uus salasõna
|
||||||
setup:
|
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_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.
|
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
|
title: Autori tunnustamine
|
||||||
challenge:
|
challenge:
|
||||||
confirm: Jätka
|
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
|
invalid_password: Vigane salasõna
|
||||||
prompt: Jätkamiseks salasõna veelkord
|
prompt: Jätkamiseks korda salasõna
|
||||||
color_scheme:
|
color_scheme:
|
||||||
auto: Auto
|
auto: Auto
|
||||||
dark: Tume
|
dark: Tume
|
||||||
@ -1627,7 +1627,7 @@ et:
|
|||||||
password: salasõna
|
password: salasõna
|
||||||
sign_in_token: e-posti turvvakood
|
sign_in_token: e-posti turvvakood
|
||||||
webauthn: turvavõtmed
|
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
|
empty: Autentimisajalugu pole saadaval
|
||||||
failed_sign_in_html: Nurjunud sisenemine meetodiga %{method} aadressilt %{ip} (%{browser})
|
failed_sign_in_html: Nurjunud sisenemine meetodiga %{method} aadressilt %{ip} (%{browser})
|
||||||
successful_sign_in_html: Edukas 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
|
inbox_url: Kopeeri soovitud sõnumivahendusserveri avalehe võrguaadress
|
||||||
irreversible: Filtreeritud postitused kaovad taastamatult, isegi kui filter on hiljem eemaldatud
|
irreversible: Filtreeritud postitused kaovad taastamatult, isegi kui filter on hiljem eemaldatud
|
||||||
locale: Kasutajaliidese, e-kirjade ja tõuketeadete keel
|
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
|
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.
|
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.
|
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.
|
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.
|
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:
|
form_challenge:
|
||||||
current_password: Turvalisse alasse sisenemine
|
current_password: Sisened turvalisse alasse
|
||||||
imports:
|
imports:
|
||||||
data: CSV fail eksporditi teisest Mastodoni serverist
|
data: CSV fail eksporditi teisest Mastodoni serverist
|
||||||
invite_request:
|
invite_request:
|
||||||
@ -214,8 +214,8 @@ et:
|
|||||||
avatar: Profiilipilt
|
avatar: Profiilipilt
|
||||||
bot: See konto on robot
|
bot: See konto on robot
|
||||||
chosen_languages: Keelte filtreerimine
|
chosen_languages: Keelte filtreerimine
|
||||||
confirm_new_password: Uue salasõna kinnitamine
|
confirm_new_password: Korda uut salasõna
|
||||||
confirm_password: Salasõna kinnitamine
|
confirm_password: Korda salasõna
|
||||||
context: Filtreeri kontekste
|
context: Filtreeri kontekste
|
||||||
current_password: Kehtiv salasõna
|
current_password: Kehtiv salasõna
|
||||||
data: Andmed
|
data: Andmed
|
||||||
|
|||||||
@ -124,6 +124,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
scope path: 'ap', as: 'ap' do
|
scope path: 'ap', as: 'ap' do
|
||||||
resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources 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
|
resources :statuses, only: [:show] do
|
||||||
member do
|
member do
|
||||||
get :activity
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
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 "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "item_count", default: 0, 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 ["account_id"], name: "index_collections_on_account_id"
|
||||||
t.index ["tag_id"], name: "index_collections_on_tag_id"
|
t.index ["tag_id"], name: "index_collections_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|||||||
@ -59,7 +59,7 @@ services:
|
|||||||
web:
|
web:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/glitch-soc/mastodon:v4.5.4
|
image: ghcr.io/glitch-soc/mastodon:v4.5.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
@ -83,7 +83,7 @@ services:
|
|||||||
# build:
|
# build:
|
||||||
# dockerfile: ./streaming/Dockerfile
|
# dockerfile: ./streaming/Dockerfile
|
||||||
# context: .
|
# context: .
|
||||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.4
|
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming/index.js
|
command: node ./streaming/index.js
|
||||||
@ -102,7 +102,7 @@ services:
|
|||||||
sidekiq:
|
sidekiq:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/glitch-soc/mastodon:v4.5.4
|
image: ghcr.io/glitch-soc/mastodon:v4.5.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|||||||
@ -17,7 +17,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
'alpha.2'
|
'alpha.3'
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerelease
|
def prerelease
|
||||||
|
|||||||
@ -16,12 +16,18 @@ RSpec.describe Collection do
|
|||||||
|
|
||||||
it { is_expected.to_not allow_value(nil).for(:discoverable) }
|
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
|
context 'when collection is remote' do
|
||||||
subject { Fabricate.build :collection, local: false }
|
subject { Fabricate.build :collection, local: false }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:uri) }
|
it { is_expected.to validate_presence_of(:uri) }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:original_number_of_items) }
|
it { is_expected.to validate_presence_of(:original_number_of_items) }
|
||||||
|
|
||||||
|
it { is_expected.to allow_value('randomstuff').for(:language) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when using a hashtag as category' do
|
context 'when using a hashtag as category' do
|
||||||
|
|||||||
@ -450,6 +450,44 @@ RSpec.describe Account::Interactions do
|
|||||||
end
|
end
|
||||||
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
|
describe '#muting?' do
|
||||||
subject { account.muting?(target_account) }
|
subject { account.muting?(target_account) }
|
||||||
|
|
||||||
|
|||||||
@ -188,4 +188,24 @@ RSpec.describe AccountPolicy do
|
|||||||
end
|
end
|
||||||
end
|
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
|
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',
|
name: 'Low-traffic bots',
|
||||||
description: 'Really nice bots, please follow',
|
description: 'Really nice bots, please follow',
|
||||||
|
language: 'en',
|
||||||
sensitive: '0',
|
sensitive: '0',
|
||||||
discoverable: '1',
|
discoverable: '1',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,9 +163,10 @@ RSpec.describe 'API Web Push Subscriptions' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /api/web/push_subscriptions/:id' do
|
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
|
it 'gracefully handles invalid nested params' do
|
||||||
put api_web_push_subscription_path(subscription), params: { data: 'invalid' }
|
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) }
|
it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a Collection' do
|
||||||
|
let(:model) { Collection.new }
|
||||||
|
|
||||||
|
it { is_expected.to eq(ActivityPub::FeaturedCollectionSerializer) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -45,4 +45,20 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do
|
|||||||
'updated' => match_api_datetime_format,
|
'updated' => match_api_datetime_format,
|
||||||
})
|
})
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@ -18,6 +18,7 @@ RSpec.describe REST::CollectionSerializer do
|
|||||||
id: 2342,
|
id: 2342,
|
||||||
name: 'Exquisite follows',
|
name: 'Exquisite follows',
|
||||||
description: 'Always worth a follow',
|
description: 'Always worth a follow',
|
||||||
|
language: 'en',
|
||||||
local: true,
|
local: true,
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
discoverable: false,
|
discoverable: false,
|
||||||
@ -31,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do
|
|||||||
'id' => '2342',
|
'id' => '2342',
|
||||||
'name' => 'Exquisite follows',
|
'name' => 'Exquisite follows',
|
||||||
'description' => 'Always worth a follow',
|
'description' => 'Always worth a follow',
|
||||||
|
'language' => 'en',
|
||||||
'local' => true,
|
'local' => true,
|
||||||
'sensitive' => true,
|
'sensitive' => true,
|
||||||
'discoverable' => false,
|
'discoverable' => false,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user