mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-11 14:30:35 +00:00
Merge commit 'c858fc77ef194be0217fd98eae84efd261dba798' into glitch-soc/merge-upstream
This commit is contained in:
@@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
|
||||
|
||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
||||
# renovate: datasource=docker depName=docker.io/ruby
|
||||
ARG RUBY_VERSION="3.4.6"
|
||||
ARG RUBY_VERSION="3.4.7"
|
||||
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||
# renovate: datasource=node-version depName=node
|
||||
ARG NODE_MAJOR_VERSION="22"
|
||||
|
||||
@@ -9,10 +9,16 @@ module Admin
|
||||
|
||||
@pending_appeals_count = Appeal.pending.async_count
|
||||
@pending_reports_count = Report.unresolved.async_count
|
||||
@pending_tags_count = Tag.pending_review.async_count
|
||||
@pending_tags_count = pending_tags.async_count
|
||||
@pending_users_count = User.pending.async_count
|
||||
@system_checks = Admin::SystemCheck.perform(current_user)
|
||||
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pending_tags
|
||||
::Trends::TagFilter.new(status: :pending_review).results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,12 +8,32 @@ import { HoverCardController } from '../hover_card_controller';
|
||||
import type { HandledLinkProps } from './handled_link';
|
||||
import { HandledLink } from './handled_link';
|
||||
|
||||
type HandledLinkStoryProps = Pick<
|
||||
HandledLinkProps,
|
||||
'href' | 'text' | 'prevText'
|
||||
> & {
|
||||
mentionAccount: 'local' | 'remote' | 'none';
|
||||
hashtagAccount: boolean;
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Status/HandledLink',
|
||||
render(args) {
|
||||
render({ mentionAccount, hashtagAccount, ...args }) {
|
||||
let mention: HandledLinkProps['mention'] | undefined;
|
||||
if (mentionAccount === 'local') {
|
||||
mention = { id: '1', acct: 'testuser' };
|
||||
} else if (mentionAccount === 'remote') {
|
||||
mention = { id: '2', acct: 'remoteuser@mastodon.social' };
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<HandledLink {...args} mentionAccountId='1' hashtagAccountId='1' />
|
||||
<HandledLink
|
||||
{...args}
|
||||
mention={mention}
|
||||
hashtagAccountId={hashtagAccount ? '1' : undefined}
|
||||
>
|
||||
<span>{args.text}</span>
|
||||
</HandledLink>
|
||||
<HashtagMenuController />
|
||||
<HoverCardController />
|
||||
</>
|
||||
@@ -22,15 +42,24 @@ const meta = {
|
||||
args: {
|
||||
href: 'https://example.com/path/subpath?query=1#hash',
|
||||
text: 'https://example.com',
|
||||
mentionAccount: 'none',
|
||||
hashtagAccount: false,
|
||||
},
|
||||
argTypes: {
|
||||
mentionAccount: {
|
||||
control: { type: 'select' },
|
||||
options: ['local', 'remote', 'none'],
|
||||
defaultValue: 'none',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<Pick<HandledLinkProps, 'href' | 'text'>>;
|
||||
} satisfies Meta<HandledLinkStoryProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -38,15 +67,23 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Simple: Story = {
|
||||
args: {
|
||||
href: 'https://example.com/test',
|
||||
},
|
||||
};
|
||||
|
||||
export const Hashtag: Story = {
|
||||
args: {
|
||||
text: '#example',
|
||||
hashtagAccount: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Mention: Story = {
|
||||
args: {
|
||||
text: '@user',
|
||||
mentionAccount: 'local',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,111 +1,109 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||
|
||||
export interface HandledLinkProps {
|
||||
href: string;
|
||||
text: string;
|
||||
prevText?: string;
|
||||
hashtagAccountId?: string;
|
||||
mentionAccountId?: string;
|
||||
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
||||
}
|
||||
|
||||
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||
href,
|
||||
text,
|
||||
prevText,
|
||||
hashtagAccountId,
|
||||
mentionAccountId,
|
||||
mention,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
// Handle hashtags
|
||||
if (text.startsWith('#')) {
|
||||
if (text.startsWith('#') || prevText?.endsWith('#')) {
|
||||
const hashtag = text.slice(1).trim();
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className='mention hashtag'
|
||||
className={classNames('mention hashtag', className)}
|
||||
to={`/tags/${hashtag}`}
|
||||
rel='tag'
|
||||
data-menu-hashtag={hashtagAccountId}
|
||||
>
|
||||
#<span>{hashtag}</span>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
} else if (text.startsWith('@')) {
|
||||
} else if ((text.startsWith('@') || prevText?.endsWith('@')) && mention) {
|
||||
// Handle mentions
|
||||
const mention = text.slice(1).trim();
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className='mention'
|
||||
to={`/@${mention}`}
|
||||
title={`@${mention}`}
|
||||
data-hover-card-account={mentionAccountId}
|
||||
className={classNames('mention', className)}
|
||||
to={`/@${mention.acct}`}
|
||||
title={`@${mention.acct}`}
|
||||
data-hover-card-account={mention.id}
|
||||
>
|
||||
@<span>{mention}</span>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Non-absolute paths treated as internal links.
|
||||
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
||||
if (href.startsWith('/')) {
|
||||
return (
|
||||
<Link {...props} className='unhandled-link' to={href}>
|
||||
{text}
|
||||
<Link className={classNames('unhandled-link', className)} to={href}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(href);
|
||||
const [first, ...rest] = url.pathname.split('/').slice(1); // Start at 1 to skip the leading slash.
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={href}
|
||||
title={href}
|
||||
className='unhandled-link'
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
translate='no'
|
||||
>
|
||||
<span className='invisible'>{url.protocol + '//'}</span>
|
||||
<span className='ellipsis'>{`${url.hostname}/${first ?? ''}`}</span>
|
||||
<span className='invisible'>{'/' + rest.join('/')}</span>
|
||||
</a>
|
||||
);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
href={href}
|
||||
title={href}
|
||||
className={classNames('unhandled-link', className)}
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
translate='no'
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const useElementHandledLink = ({
|
||||
hashtagAccountId,
|
||||
hrefToMentionAccountId,
|
||||
hrefToMention,
|
||||
}: {
|
||||
hashtagAccountId?: string;
|
||||
hrefToMentionAccountId?: (href: string) => string | undefined;
|
||||
hrefToMention?: (href: string) => ApiMentionJSON | undefined;
|
||||
} = {}) => {
|
||||
const onElement = useCallback<OnElementHandler>(
|
||||
(element, { key, ...props }) => {
|
||||
(element, { key, ...props }, children) => {
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
const mentionId = hrefToMentionAccountId?.(element.href);
|
||||
const mention = hrefToMention?.(element.href);
|
||||
return (
|
||||
<HandledLink
|
||||
{...props}
|
||||
key={key as string} // React requires keys to not be part of spread props.
|
||||
href={element.href}
|
||||
text={element.innerText}
|
||||
prevText={element.previousSibling?.textContent ?? undefined}
|
||||
hashtagAccountId={hashtagAccountId}
|
||||
mentionAccountId={mentionId}
|
||||
/>
|
||||
mention={mention}
|
||||
>
|
||||
{children}
|
||||
</HandledLink>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[hashtagAccountId, hrefToMentionAccountId],
|
||||
[hashtagAccountId, hrefToMention],
|
||||
);
|
||||
return { onElement };
|
||||
};
|
||||
|
||||
@@ -204,7 +204,7 @@ class StatusContent extends PureComponent {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
handleElement = (element, { key, ...props }) => {
|
||||
handleElement = (element, { key, ...props }, children) => {
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
|
||||
return (
|
||||
@@ -213,9 +213,11 @@ class StatusContent extends PureComponent {
|
||||
href={element.href}
|
||||
text={element.innerText}
|
||||
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
||||
mentionAccountId={mention?.get('id')}
|
||||
mention={mention?.toJSON()}
|
||||
key={key}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</HandledLink>
|
||||
);
|
||||
} else if (element instanceof HTMLParagraphElement && element.classList.contains('quote-inline')) {
|
||||
return null;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||
|
||||
export const AuthorLink = ({ accountId }) => {
|
||||
const account = useAppSelector(state => state.getIn(['accounts', accountId]));
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkedDisplayName displayProps={{account}} className='story__details__shared__author-link'>
|
||||
<Avatar account={account} size={16} />
|
||||
</LinkedDisplayName>
|
||||
);
|
||||
};
|
||||
|
||||
AuthorLink.propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
export const AuthorLink: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkedDisplayName
|
||||
displayProps={{ account, variant: 'simple' }}
|
||||
className='story__details__shared__author-link'
|
||||
>
|
||||
<Avatar account={account} size={16} />
|
||||
</LinkedDisplayName>
|
||||
);
|
||||
};
|
||||
@@ -48,9 +48,8 @@ export const EmbeddedStatusContent: React.FC<{
|
||||
);
|
||||
const htmlHandlers = useElementHandledLink({
|
||||
hashtagAccountId: status.get('account') as string | undefined,
|
||||
hrefToMentionAccountId(href) {
|
||||
const mention = mentions.find((item) => item.url === href);
|
||||
return mention?.id;
|
||||
hrefToMention(href) {
|
||||
return mentions.find((item) => item.url === href);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Përshkruajeni këtë për persona me mangësi shikimi…",
|
||||
"alt_text_modal.done": "U bë",
|
||||
"announcement.announcement": "Lajmërim",
|
||||
"annual_report.summary.archetype.oracle": "Orakulli",
|
||||
"annual_report.summary.followers.followers": "ndjekës",
|
||||
"annual_report.summary.followers.total": "{count} gjithsej",
|
||||
"annual_report.summary.here_it_is": "Ja {year} juaj e shqyrtuar:",
|
||||
@@ -299,6 +300,7 @@
|
||||
"domain_pill.your_handle": "Targa juaj:",
|
||||
"domain_pill.your_server": "Shtëpia juaj dixhitale, kur gjenden krejt postimet tuaja. S’ju pëlqen kjo këtu? Shpërngulni shërbyes kur të doni dhe sillni edhe ndjekësit tuaj.",
|
||||
"domain_pill.your_username": "Identifikuesi juja unik në këtë shërbyes. Është e mundur të gjenden përdorues me të njëjtin emër përdoruesi në shërbyes të ndryshëm.",
|
||||
"dropdown.empty": "Përzgjidhni një mundësi",
|
||||
"embed.instructions": "Trupëzojeni këtë gjendje në sajtin tuaj duke kopjuar kodin më poshtë.",
|
||||
"embed.preview": "Ja si do të duket:",
|
||||
"emoji_button.activity": "Veprimtari",
|
||||
@@ -568,6 +570,8 @@
|
||||
"navigation_bar.follows_and_followers": "Ndjekje dhe ndjekës",
|
||||
"navigation_bar.import_export": "Importim dhe eksportim",
|
||||
"navigation_bar.lists": "Lista",
|
||||
"navigation_bar.live_feed_local": "Pryrje e atypëratyshme (vendore)",
|
||||
"navigation_bar.live_feed_public": "Prurje e atypëratyshme (publike)",
|
||||
"navigation_bar.logout": "Dalje",
|
||||
"navigation_bar.moderation": "Moderim",
|
||||
"navigation_bar.more": "Më tepër",
|
||||
@@ -737,11 +741,18 @@
|
||||
"privacy.private.short": "Ndjekës",
|
||||
"privacy.public.long": "Cilido që hyn e del në Mastodon",
|
||||
"privacy.public.short": "Publik",
|
||||
"privacy.quote.anyone": "{visibility}, mund të citojë cilido",
|
||||
"privacy.quote.disabled": "{visibility}, citimet janë çaktivizuar",
|
||||
"privacy.quote.limited": "{visibility}, citime të kufizuara",
|
||||
"privacy.unlisted.additional": "Ky sillet saktësisht si publik, vetëm se postimi s’do të shfaqet në prurje të drejtpërdrejta, ose në hashtag-ë, te eksploroni, apo kërkim në Mastodon, edhe kur keni zgjedhur të jetë për tërë llogarinë.",
|
||||
"privacy.unlisted.long": "Fshehur nga përfundime kërkimi në Mastodon, rrjedha kohore gjërash në modë dhe publike",
|
||||
"privacy.unlisted.short": "Publik i qetë",
|
||||
"privacy_policy.last_updated": "Përditësuar së fundi më {date}",
|
||||
"privacy_policy.title": "Rregulla Privatësie",
|
||||
"quote_error.poll": "Me pyetësorët nuk lejohet citim.",
|
||||
"quote_error.quote": "Lejohet vetëm një citim në herë.",
|
||||
"quote_error.unauthorized": "S’jen i autorizuar ta citoni këtë postim.",
|
||||
"quote_error.upload": "Me bashkëngjitjet media nuk lejohet citim.",
|
||||
"recommended": "E rekomanduar",
|
||||
"refresh": "Rifreskoje",
|
||||
"regeneration_indicator.please_stand_by": "Ju lutemi, mos u largoni.",
|
||||
@@ -851,6 +862,7 @@
|
||||
"status.admin_account": "Hap ndërfaqe moderimi për @{name}",
|
||||
"status.admin_domain": "Hap ndërfaqe moderimi për {domain}",
|
||||
"status.admin_status": "Hape këtë mesazh te ndërfaqja e moderimit",
|
||||
"status.all_disabled": "Përforcimet dhe citime janë të çaktivizuara",
|
||||
"status.block": "Blloko @{name}",
|
||||
"status.bookmark": "Faqeruaje",
|
||||
"status.cancel_reblog_private": "Shpërforcojeni",
|
||||
@@ -889,6 +901,8 @@
|
||||
"status.mute_conversation": "Heshtoje bisedën",
|
||||
"status.open": "Zgjeroje këtë mesazh",
|
||||
"status.pin": "Fiksoje në profil",
|
||||
"status.quote": "Citojeni",
|
||||
"status.quote.cancel": "Anuloje citimin",
|
||||
"status.quote_error.filtered": "Fshehur për shkak të njërit nga filtrat tuaj",
|
||||
"status.quote_error.limited_account_hint.action": "Shfaqe, sido qoftë",
|
||||
"status.quote_error.limited_account_hint.title": "Kjo llogari është fshehur nga moderatorët e {domain}.",
|
||||
@@ -899,7 +913,9 @@
|
||||
"status.quote_followers_only": "Këtë postim mund ta citojnë vetëm ndjekës",
|
||||
"status.quote_manual_review": "Autori do ta shqyrtojë dorazi",
|
||||
"status.quote_noun": "Citim",
|
||||
"status.quote_policy_change": "Ndryshoni cilët mund të citojnë",
|
||||
"status.quote_post_author": "U citua një postim nga @{name}",
|
||||
"status.quote_private": "Postimet private s’mund të citohen",
|
||||
"status.quotes": "{count, plural, one {citim} other {citime}}",
|
||||
"status.quotes.empty": "Këtë postim ende s’e ka cituar kush. Kur dikush ta bëjë, do të shfaqet këtu.",
|
||||
"status.quotes.local_other_disclaimer": "Citimet e hedhura poshtë nga autori s’do të shfaqen.",
|
||||
@@ -959,6 +975,7 @@
|
||||
"upload_button.label": "Shtoni figura, një video ose një kartelë audio",
|
||||
"upload_error.limit": "U tejkalua kufi ngarkimi kartelash.",
|
||||
"upload_error.poll": "Me pyetësorët s’lejohet ngarkim kartelash.",
|
||||
"upload_error.quote": "Nuk lejohet ngarkim kartelash me citime.",
|
||||
"upload_form.drag_and_drop.instructions": "Që të merrni një bashkëngjitje media, shtypni tastin Space ose Enter. Teksa bëhet tërheqje, përdorni tastet shigjetë për ta shpënë bashkëngjitjen media në cilëndo drejtori që doni. Shtypni sërish Space ose Enter që të lihet bashkëngjitja media në pozicionin e vet të ri, ose shtypni Esc, që të anulohet veprimi.",
|
||||
"upload_form.drag_and_drop.on_drag_cancel": "Tërheqja u anulua. Bashkëngjitja media {item} u la.",
|
||||
"upload_form.drag_and_drop.on_drag_end": "Bashkëngjitja media {item} u la.",
|
||||
@@ -982,13 +999,18 @@
|
||||
"video.unmute": "Hiqi heshtimin",
|
||||
"video.volume_down": "Ulje volumi",
|
||||
"video.volume_up": "Ngritje volumi",
|
||||
"visibility_modal.button_title": "Caktoni dukshmëri",
|
||||
"visibility_modal.header": "Dukshmëri dhe ndërveprim",
|
||||
"visibility_modal.helper.direct_quoting": "Përmendje private të krijuara në Mastodon s’mund të citohen nga të tjerë.",
|
||||
"visibility_modal.helper.privacy_editing": "Dukshmëria s’mund të ndryshohet pasi postimi botohet.",
|
||||
"visibility_modal.helper.privacy_private_self_quote": "Citimet nga ju vetë të postime private s’mund të bëhen publike.",
|
||||
"visibility_modal.helper.private_quoting": "Postime vetëm për ndjekësit, të krijuara në Mastodon s’mund të citohen nga të tjerë.",
|
||||
"visibility_modal.helper.unlisted_quoting": "Kur njerëzit ju citojnë, nga rrjedha kohore e gjërave në modë do të kalohen si të fshehura edhe postimet e tyre.",
|
||||
"visibility_modal.instructions": "Kontrolloni cilët mund të ndërveprojnë me këtë postim. Rregullime mund të aplikooni edhe mbi krejt postimet e ardshme, që nga <link>Parapëlqime > Parazgjedhje postimi</link>.",
|
||||
"visibility_modal.privacy_label": "Dukshmëri",
|
||||
"visibility_modal.quote_followers": "Vetëm ndjekës",
|
||||
"visibility_modal.quote_label": "Cilët mund të citojnë",
|
||||
"visibility_modal.quote_nobody": "Thjesht unë",
|
||||
"visibility_modal.quote_public": "Cilido",
|
||||
"visibility_modal.save": "Ruaje"
|
||||
}
|
||||
|
||||
@@ -218,11 +218,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
|
||||
def process_quote
|
||||
@quote_uri = @status_parser.quote_uri
|
||||
return if @quote_uri.blank?
|
||||
return unless @status_parser.quote?
|
||||
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
|
||||
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
|
||||
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending)
|
||||
end
|
||||
|
||||
def process_hashtag(tag)
|
||||
|
||||
@@ -120,6 +120,14 @@ class ActivityPub::Parser::StatusParser
|
||||
flags
|
||||
end
|
||||
|
||||
def quote?
|
||||
%w(quote _misskey_quote quoteUrl quoteUri).any? { |key| @object[key].present? }
|
||||
end
|
||||
|
||||
def deleted_quote?
|
||||
@object['quote'].is_a?(Hash) && @object['quote']['type'] == 'Tombstone'
|
||||
end
|
||||
|
||||
def quote_uri
|
||||
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
|
||||
value_or_id(as_array(@object[key]).first)
|
||||
|
||||
@@ -25,7 +25,7 @@ class Quote < ApplicationRecord
|
||||
REFRESH_DEADLINE = 6.hours
|
||||
|
||||
enum :state,
|
||||
{ pending: 0, accepted: 1, rejected: 2, revoked: 3 },
|
||||
{ pending: 0, accepted: 1, rejected: 2, revoked: 3, deleted: 4 },
|
||||
validate: true
|
||||
|
||||
belongs_to :status
|
||||
|
||||
@@ -34,8 +34,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
attribute :voters_count, if: :poll_and_voters_count?
|
||||
|
||||
attribute :quote, if: :quote?
|
||||
attribute :quote, key: :_misskey_quote, if: :quote?
|
||||
attribute :quote, key: :quote_uri, if: :quote?
|
||||
attribute :quote, key: :_misskey_quote, if: :serializable_quote?
|
||||
attribute :quote, key: :quote_uri, if: :serializable_quote?
|
||||
attribute :quote_authorization, if: :quote_authorization?
|
||||
|
||||
attribute :interaction_policy
|
||||
@@ -226,13 +226,17 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
object.quote&.present?
|
||||
end
|
||||
|
||||
def serializable_quote?
|
||||
object.quote&.quoted_status&.present?
|
||||
end
|
||||
|
||||
def quote_authorization?
|
||||
object.quote.present? && ActivityPub::TagManager.instance.approval_uri_for(object.quote).present?
|
||||
end
|
||||
|
||||
def quote
|
||||
# TODO: handle inlining self-quotes
|
||||
ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status)
|
||||
object.quote.quoted_status.present? ? ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status) : { type: 'Tombstone' }
|
||||
end
|
||||
|
||||
def quote_authorization
|
||||
|
||||
@@ -74,6 +74,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
update_quote_approval!
|
||||
update_counts!
|
||||
end
|
||||
|
||||
broadcast_updates! if @status.quote&.state_previously_changed?
|
||||
end
|
||||
|
||||
def update_interaction_policies!
|
||||
@@ -298,7 +300,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
def update_quote!
|
||||
quote_uri = @status_parser.quote_uri
|
||||
|
||||
if quote_uri.present?
|
||||
if @status_parser.quote?
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri)
|
||||
|
||||
@@ -308,7 +310,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
# Revoke the quote while we get a chance… maybe this should be a `before_destroy` hook?
|
||||
RevokeQuoteService.new.call(@status.quote) if @status.quote.quoted_account&.local? && @status.quote.accepted?
|
||||
@status.quote.destroy
|
||||
quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
|
||||
quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending)
|
||||
@quote_changed = true
|
||||
else
|
||||
quote = @status.quote
|
||||
|
||||
@@ -10,6 +10,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker
|
||||
def perform(quote_id, quoted_uri, options = {})
|
||||
quote = Quote.find(quote_id)
|
||||
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id])
|
||||
::DistributionWorker.perform_async(quote.status_id, { 'update' => true }) if quote.state_previously_changed?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Do nothing
|
||||
true
|
||||
|
||||
@@ -148,6 +148,9 @@ sq:
|
||||
min_age: S’duhet të jetë nën moshën minimum të domosdoshme nga ligjet në juridiksionin tuaj.
|
||||
user:
|
||||
chosen_languages: Në iu vëntë shenjë, te rrjedha kohore publike do të shfaqen vetëm mesazhe në gjuhët e përzgjedhura
|
||||
date_of_birth:
|
||||
one: Na duhet të sigurohemi se jeni të paktën %{count} që të përdorni %{domain}. S’do ta depozitojmë këtë.
|
||||
other: Na duhet të sigurohemi se jeni të paktën %{count} që të përdorni %{domain}. S’do ta depozitojmë këtë.
|
||||
role: Roli kontrollon cilat leje ka përdoruesi.
|
||||
user_role:
|
||||
color: Ngjyrë për t’u përdorur për rolin nëpër UI, si RGB në format gjashtëmbëdhjetësh
|
||||
@@ -237,6 +240,7 @@ sq:
|
||||
setting_display_media_default: Parazgjedhje
|
||||
setting_display_media_hide_all: Fshihi krejt
|
||||
setting_display_media_show_all: Shfaqi krejt
|
||||
setting_emoji_style: Stil emoji-sh
|
||||
setting_expand_spoilers: Mesazhet me sinjalizime mbi lëndën, zgjeroji përherë
|
||||
setting_hide_network: Fshiheni rrjetin tuaj
|
||||
setting_missing_alt_text_modal: Shfaq dialog ripohimi, para postimi mediash pa tekst alternativ
|
||||
@@ -273,12 +277,16 @@ sq:
|
||||
content_cache_retention_period: Periudhë mbajtjeje lënde të largët
|
||||
custom_css: CSS Vetjake
|
||||
favicon: Favikonë
|
||||
local_live_feed_access: Hyrje te prurje të atypëratyshme që përmbajnë postime vendore
|
||||
local_topic_feed_access: Hyrje te prurje hashtag-ësh dhe lidhjesh që përmbajnë postime vendore
|
||||
mascot: Simbol vetjak (e dikurshme)
|
||||
media_cache_retention_period: Periudhë mbajtjeje lënde media
|
||||
min_age: Domosdosmëri moshe minimum
|
||||
peers_api_enabled: Publiko te API listë shërbyesish të zbuluar
|
||||
profile_directory: Aktivizo drejtori profilesh
|
||||
registrations_mode: Kush mund të regjistrohet
|
||||
remote_live_feed_access: Hyrje te prurje të atypëratyshme që përmbajnë postime nga larg
|
||||
remote_topic_feed_access: Hyrje te prurje hashtag-ësh dhe lidhjesh që përmbajnë postime nga larg
|
||||
require_invite_text: Kërko një arsye për pjesëmarrje
|
||||
show_domain_blocks: Shfaq bllokime përkatësish
|
||||
show_domain_blocks_rationale: Shfaq pse janë bllokuar përkatësitë
|
||||
@@ -365,6 +373,10 @@ sq:
|
||||
name: Emër
|
||||
permissions_as_keys: Leje
|
||||
position: Përparësi
|
||||
username_block:
|
||||
allow_with_approval: Lejo regjistrim me miratim
|
||||
comparison: Metodë krahasimi
|
||||
username: Fjalë për t’u vëzhguar
|
||||
webhook:
|
||||
events: Akte të aktivizuar
|
||||
template: Gjedhe ngarkese
|
||||
|
||||
@@ -190,6 +190,7 @@ sq:
|
||||
create_relay: Krijoni Rele
|
||||
create_unavailable_domain: Krijo Përkatësi të Papërdorshme
|
||||
create_user_role: Krijoni Rol
|
||||
create_username_block: Krijoni Rregull Emrash Përdoruesish
|
||||
demote_user: Zhgradoje Përdoruesin
|
||||
destroy_announcement: Fshije Lajmërimin
|
||||
destroy_canonical_email_block: Fshini Bllokim Email-esh
|
||||
@@ -203,6 +204,7 @@ sq:
|
||||
destroy_status: Fshi Gjendje
|
||||
destroy_unavailable_domain: Fshi Përkatësi të Papërdorshme
|
||||
destroy_user_role: Asgjësoje Rolin
|
||||
destroy_username_block: Fshini Rregull Emrash Përdoruesish
|
||||
disable_2fa_user: Çaktivizo 2FA-në
|
||||
disable_custom_emoji: Çaktivizo Emotikon Vetjak
|
||||
disable_relay: Çaktivizoje Relenë
|
||||
@@ -237,6 +239,7 @@ sq:
|
||||
update_report: Përditësoni Raportimin
|
||||
update_status: Përditëso Gjendjen
|
||||
update_user_role: Përditësoni Rol
|
||||
update_username_block: Përditësoni Rregull Emrash Përdoruesish
|
||||
actions:
|
||||
approve_appeal_html: "%{name} miratoi apelim vendimi moderimi nga %{target}"
|
||||
approve_user_html: "%{name} miratoi regjistrim nga %{target}"
|
||||
@@ -255,6 +258,7 @@ sq:
|
||||
create_relay_html: "%{name} krijoi një rele %{target}"
|
||||
create_unavailable_domain_html: "%{name} ndali dërgimin drejt përkatësisë %{target}"
|
||||
create_user_role_html: "%{name} krijoi rolin %{target}"
|
||||
create_username_block_html: "%{name} shtoi rregull për emra përdoruesish që përmbajnë %{target}"
|
||||
demote_user_html: "%{name} zhgradoi përdoruesin %{target}"
|
||||
destroy_announcement_html: "%{name} fshiu lajmërimin për %{target}"
|
||||
destroy_canonical_email_block_html: "%{name} zhbllokoi email me hashin %{target}"
|
||||
@@ -268,6 +272,7 @@ sq:
|
||||
destroy_status_html: "%{name} hoqi gjendje nga %{target}"
|
||||
destroy_unavailable_domain_html: "%{name} rinisi dërgimin drejt përkatësisë %{target}"
|
||||
destroy_user_role_html: "%{name} fshiu rolin %{target}"
|
||||
destroy_username_block_html: "%{name} hoqi rregull për emra përdoruesish që përmbajnë %{target}"
|
||||
disable_2fa_user_html: "%{name} çaktivizoi domosdoshmërinë për dyfaktorësh për përdoruesin %{target}"
|
||||
disable_custom_emoji_html: "%{name} çaktivizoi emoxhin %{target}"
|
||||
disable_relay_html: "%{name} çaktivizoi relenë %{target}"
|
||||
@@ -302,6 +307,7 @@ sq:
|
||||
update_report_html: "%{name} përditësoi raportimin %{target}"
|
||||
update_status_html: "%{name} përditësoi gjendjen me %{target}"
|
||||
update_user_role_html: "%{name} ndryshoi rolin për %{target}"
|
||||
update_username_block_html: "%{name} përditësoi rregull për emra përdoruesish që përmbajnë %{target}"
|
||||
deleted_account: fshiu llogarinë
|
||||
empty: S’u gjetën regjistra.
|
||||
filter_by_action: Filtroji sipas veprimit
|
||||
@@ -505,6 +511,7 @@ sq:
|
||||
select_capabilities: Përzgjidhni Aftësi
|
||||
sign_in: Hyni
|
||||
status: Gjendje
|
||||
title: Shërbyes Shërbimesh Ndihmëse Fediversi
|
||||
title: FASP
|
||||
follow_recommendations:
|
||||
description_html: "<strong>Rekomandimet për ndjekje ndihmojnë përdoruesit e rinj të gjejnë shpejt lëndë me interes</strong>. Kur një përdorues nuk ka ndërvepruar mjaftueshëm me të tjerët, që të formohen rekomandime të personalizuara ndjekjeje, rekomandohen këto llogari. Ato përzgjidhen çdo ditë, prej një përzierje llogarish me shkallën më të lartë të angazhimit dhe numrin më të lartë të ndjekësve vendorë për një gjuhë të dhënë."
|
||||
@@ -1090,6 +1097,14 @@ sq:
|
||||
delete: Fshije
|
||||
edit:
|
||||
title: Përpunoni rregull emrash përdoruesi
|
||||
matches_exactly_html: Baras me %{string}
|
||||
new:
|
||||
create: Krijoni rregull
|
||||
title: Krijoni rregull të ri emrash përdoruesish
|
||||
no_username_block_selected: S’u ndryshua ndonjë rregull emrash përdoruesishm ngaqë s’u përzgjodh ndonjë
|
||||
not_permitted: Jo i lejuar
|
||||
title: Rregulla emrash përdoruesish
|
||||
updated_msg: Rregulli i emrave të përdoruesve u përditësua me sukses
|
||||
warning_presets:
|
||||
add_new: Shtoni të ri
|
||||
delete: Fshije
|
||||
@@ -1880,6 +1895,7 @@ sq:
|
||||
edited_at_html: Përpunuar më %{date}
|
||||
errors:
|
||||
in_reply_not_found: Gjendja të cilës po provoni t’i përgjigjeni s’duket se ekziston.
|
||||
quoted_status_not_found: Postimi që po rrekeni të citoni nuk duket se ekziston.
|
||||
over_character_limit: u tejkalua kufi shenjash prej %{max}
|
||||
pin_errors:
|
||||
direct: Postimet që janë të dukshme vetëm për përdoruesit e përmendur s’mund të fiksohen
|
||||
|
||||
@@ -988,6 +988,30 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unverifiable quote of a dead post' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
type: 'Note',
|
||||
content: 'woah what she said is amazing',
|
||||
quote: { type: 'Tombstone' }
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates a status with an unverified quote' do
|
||||
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
expect(status).to_not be_nil
|
||||
expect(status.quote).to_not be_nil
|
||||
expect(status.quote).to have_attributes(
|
||||
state: 'deleted',
|
||||
approval_uri: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unverifiable unknown post' do
|
||||
let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' }
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a deleted quote' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
|
||||
before do
|
||||
Fabricate(:quote, status: parent, quoted_status: nil, state: :accepted)
|
||||
end
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
'type' => 'Note',
|
||||
'quote' => { 'type' => 'Tombstone' },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a quote policy' do
|
||||
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||
|
||||
|
||||
@@ -1053,6 +1053,44 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status swaps a verified quote with an ID-less Tombstone through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||
let(:second_quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) }
|
||||
let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quote',
|
||||
'@type': '@id',
|
||||
},
|
||||
{
|
||||
'@id': 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||
'@type': '@id',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
summary: 'Show more',
|
||||
content: 'Hello universe',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
quote: { type: 'Tombstone' },
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the URI and unverifies the quote' do
|
||||
expect { subject.call(status, json, json) }
|
||||
.to change { status.quote.quoted_status }.from(quoted_status).to(nil)
|
||||
.and change { status.quote.state }.from('accepted').to('deleted')
|
||||
|
||||
expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status swaps a verified quote with another verifiable quote through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||
let(:second_quoted_account) { Fabricate(:account, domain: 'second-quoted.example.com') }
|
||||
|
||||
@@ -9,6 +9,7 @@ RSpec.describe 'Admin Dashboard' do
|
||||
before do
|
||||
stub_system_checks
|
||||
Fabricate :software_update
|
||||
Fabricate :tag, requested_review_at: 5.minutes.ago
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
@@ -18,6 +19,7 @@ RSpec.describe 'Admin Dashboard' do
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.dashboard.title'))
|
||||
.and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html'))
|
||||
.and have_content('0 pending hashtags')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
Reference in New Issue
Block a user