mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 16:59:41 +00:00
Merge remote-tracking branch 'upstream/stable-4.4' into HEAD
Conflicts: - `app/views/layouts/application.html.haml`: Conflict because of glitch-soc's theming system. Updated the line as upstream did.
This commit is contained in:
@@ -102,6 +102,16 @@ module ApplicationHelper
|
|||||||
policy(record).public_send(:"#{action}?")
|
policy(record).public_send(:"#{action}?")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
|
||||||
|
if condition && !current_page?(block_given? ? name : options)
|
||||||
|
link_to(name, options, html_options, &block)
|
||||||
|
elsif block_given?
|
||||||
|
content_tag(:span, options, html_options, &block)
|
||||||
|
else
|
||||||
|
content_tag(:span, name, html_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def material_symbol(icon, attributes = {})
|
def material_symbol(icon, attributes = {})
|
||||||
safe_join(
|
safe_join(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -96,12 +96,17 @@ export const ensureComposeIsVisible = (getState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function setComposeToStatus(status, text, spoiler_text) {
|
export function setComposeToStatus(status, text, spoiler_text) {
|
||||||
return{
|
return (dispatch, getState) => {
|
||||||
|
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
type: COMPOSE_SET_STATUS,
|
type: COMPOSE_SET_STATUS,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
spoiler_text,
|
spoiler_text,
|
||||||
};
|
maxOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
@@ -183,7 +188,7 @@ export function directCompose(account) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitCompose() {
|
export function submitCompose(successCallback) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const status = getState().getIn(['compose', 'text'], '');
|
const status = getState().getIn(['compose', 'text'], '');
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
@@ -239,6 +244,9 @@ export function submitCompose() {
|
|||||||
|
|
||||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
if (typeof successCallback === 'function') {
|
||||||
|
successCallback(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
// To make the app more responsive, immediately push the status
|
// To make the app more responsive, immediately push the status
|
||||||
// into the columns
|
// into the columns
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { browserHistory } from 'mastodon/components/router';
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedAccount } from './importer';
|
||||||
import { fetchContext } from './statuses_typed';
|
import { fetchContext } from './statuses_typed';
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
|
|
||||||
@@ -48,7 +48,18 @@ export function fetchStatusRequest(id, skipLoading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {boolean} [options.forceFetch]
|
||||||
|
* @param {boolean} [options.alsoFetchContext]
|
||||||
|
* @param {string | null | undefined} [options.parentQuotePostId]
|
||||||
|
*/
|
||||||
|
export function fetchStatus(id, {
|
||||||
|
forceFetch = false,
|
||||||
|
alsoFetchContext = true,
|
||||||
|
parentQuotePostId,
|
||||||
|
} = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||||
|
|
||||||
@@ -66,7 +77,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
|||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,21 +89,27 @@ export function fetchStatusSuccess(skipLoading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatusFail(id, error, skipLoading) {
|
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_FAIL,
|
type: STATUS_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
|
parentQuotePostId,
|
||||||
skipLoading,
|
skipLoading,
|
||||||
skipAlert: true,
|
skipAlert: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redraft(status, raw_text) {
|
export function redraft(status, raw_text) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
|
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
type: REDRAFT,
|
type: REDRAFT,
|
||||||
status,
|
status,
|
||||||
raw_text,
|
raw_text,
|
||||||
|
maxOptions,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { apiRequestPost } from 'mastodon/api';
|
import { apiRequestPost } from 'mastodon/api';
|
||||||
import type { Status, StatusVisibility } from 'mastodon/models/status';
|
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||||
|
import type { StatusVisibility } from 'mastodon/models/status';
|
||||||
|
|
||||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||||
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
|
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, {
|
||||||
visibility,
|
visibility,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUnreblog = (statusId: string) =>
|
export const apiUnreblog = (statusId: string) =>
|
||||||
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
|
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`);
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ const QuoteWrapper: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NestedQuoteLink: React.FC<{
|
const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => {
|
||||||
status: Status;
|
|
||||||
}> = ({ status }) => {
|
|
||||||
const accountId = status.get('account') as string;
|
const accountId = status.get('account') as string;
|
||||||
const account = useAppSelector((state) =>
|
const account = useAppSelector((state) =>
|
||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
@@ -78,21 +76,40 @@ type GetStatusSelector = (
|
|||||||
export const QuotedStatus: React.FC<{
|
export const QuotedStatus: React.FC<{
|
||||||
quote: QuoteMap;
|
quote: QuoteMap;
|
||||||
contextType?: string;
|
contextType?: string;
|
||||||
|
parentQuotePostId?: string | null;
|
||||||
variant?: 'full' | 'link';
|
variant?: 'full' | 'link';
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
|
}> = ({
|
||||||
|
quote,
|
||||||
|
contextType,
|
||||||
|
parentQuotePostId,
|
||||||
|
nestingLevel = 1,
|
||||||
|
variant = 'full',
|
||||||
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const quoteState = useAppSelector((state) =>
|
||||||
|
parentQuotePostId
|
||||||
|
? state.statuses.getIn([parentQuotePostId, 'quote', 'state'])
|
||||||
|
: quote.get('state'),
|
||||||
|
);
|
||||||
|
|
||||||
const quotedStatusId = quote.get('quoted_status');
|
const quotedStatusId = quote.get('quoted_status');
|
||||||
const quoteState = quote.get('state');
|
|
||||||
const status = useAppSelector((state) =>
|
const status = useAppSelector((state) =>
|
||||||
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!status && quotedStatusId) {
|
if (shouldLoadQuote && quotedStatusId) {
|
||||||
dispatch(fetchStatus(quotedStatusId));
|
dispatch(
|
||||||
|
fetchStatus(quotedStatusId, {
|
||||||
|
parentQuotePostId,
|
||||||
|
alsoFetchContext: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [status, quotedStatusId, dispatch]);
|
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
||||||
|
|
||||||
// In order to find out whether the quoted post should be completely hidden
|
// In order to find out whether the quoted post should be completely hidden
|
||||||
// due to a matching filter, we run it through the selector used by `status_container`.
|
// due to a matching filter, we run it through the selector used by `status_container`.
|
||||||
@@ -173,6 +190,7 @@ export const QuotedStatus: React.FC<{
|
|||||||
{canRenderChildQuote && (
|
{canRenderChildQuote && (
|
||||||
<QuotedStatus
|
<QuotedStatus
|
||||||
quote={childQuote}
|
quote={childQuote}
|
||||||
|
parentQuotePostId={quotedStatusId}
|
||||||
contextType={contextType}
|
contextType={contextType}
|
||||||
variant={
|
variant={
|
||||||
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
||||||
@@ -208,7 +226,11 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
|
|||||||
if (quote) {
|
if (quote) {
|
||||||
return (
|
return (
|
||||||
<StatusContainer {...props}>
|
<StatusContainer {...props}>
|
||||||
<QuotedStatus quote={quote} contextType={props.contextType} />
|
<QuotedStatus
|
||||||
|
quote={quote}
|
||||||
|
parentQuotePostId={status?.get('id') as string}
|
||||||
|
contextType={props.contextType}
|
||||||
|
/>
|
||||||
</StatusContainer>
|
</StatusContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
maxChars: PropTypes.number,
|
maxChars: PropTypes.number,
|
||||||
|
redirectOnSuccess: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const mapStateToProps = state => ({
|
|||||||
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch, props) => ({
|
||||||
|
|
||||||
onChange (text) {
|
onChange (text) {
|
||||||
dispatch(changeCompose(text));
|
dispatch(changeCompose(text));
|
||||||
@@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
modalProps: {},
|
modalProps: {},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
dispatch(submitCompose());
|
dispatch(submitCompose((status) => {
|
||||||
|
if (props.redirectOnSuccess) {
|
||||||
|
window.location.assign(status.url);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ModalContainer from 'mastodon/features/ui/containers/modal_container';
|
|||||||
|
|
||||||
const Compose = () => (
|
const Compose = () => (
|
||||||
<>
|
<>
|
||||||
<ComposeFormContainer autoFocus withoutNavigation />
|
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
|
||||||
<AlertsController />
|
<AlertsController />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<LoadingBarContainer className='loading-bar' />
|
<LoadingBarContainer className='loading-bar' />
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
|
|||||||
const dispatchRenderSignal = useRenderSignal();
|
const dispatchRenderSignal = useRenderSignal();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchStatus(id, false, false));
|
dispatch(fetchStatus(id, { alsoFetchContext: false }));
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
const handleToggleHidden = useCallback(() => {
|
const handleToggleHidden = useCallback(() => {
|
||||||
|
|||||||
@@ -381,7 +381,10 @@ export const DetailedStatus: React.FC<{
|
|||||||
{hashtagBar}
|
{hashtagBar}
|
||||||
|
|
||||||
{status.get('quote') && (
|
{status.get('quote') && (
|
||||||
<QuotedStatus quote={status.get('quote')} />
|
<QuotedStatus
|
||||||
|
quote={status.get('quote')}
|
||||||
|
parentQuotePostId={status.get('id')}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent {
|
|||||||
|
|
||||||
handleSuccess = () => {
|
handleSuccess = () => {
|
||||||
const { dispatch, statusId } = this.props;
|
const { dispatch, statusId } = this.props;
|
||||||
dispatch(fetchStatus(statusId, true));
|
dispatch(fetchStatus(statusId, {forceFetch: true}));
|
||||||
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
|
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -501,8 +501,13 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action.status.get('poll')) {
|
if (action.status.get('poll')) {
|
||||||
|
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||||
|
if (options.size < action.maxOptions) {
|
||||||
|
options = options.push('');
|
||||||
|
}
|
||||||
|
|
||||||
map.set('poll', ImmutableMap({
|
map.set('poll', ImmutableMap({
|
||||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
options: options,
|
||||||
multiple: action.status.get('poll').multiple,
|
multiple: action.status.get('poll').multiple,
|
||||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||||
}));
|
}));
|
||||||
@@ -530,8 +535,13 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action.status.get('poll')) {
|
if (action.status.get('poll')) {
|
||||||
|
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||||
|
if (options.size < action.maxOptions) {
|
||||||
|
options = options.push('');
|
||||||
|
}
|
||||||
|
|
||||||
map.set('poll', ImmutableMap({
|
map.set('poll', ImmutableMap({
|
||||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
options: options,
|
||||||
multiple: action.status.get('poll').multiple,
|
multiple: action.status.get('poll').multiple,
|
||||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -73,8 +73,15 @@ export default function statuses(state = initialState, action) {
|
|||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STATUS_FETCH_REQUEST:
|
case STATUS_FETCH_REQUEST:
|
||||||
return state.setIn([action.id, 'isLoading'], true);
|
return state.setIn([action.id, 'isLoading'], true);
|
||||||
case STATUS_FETCH_FAIL:
|
case STATUS_FETCH_FAIL: {
|
||||||
|
if (action.parentQuotePostId && action.error.status === 404) {
|
||||||
|
return state
|
||||||
|
.delete(action.id)
|
||||||
|
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||||
|
} else {
|
||||||
return state.delete(action.id);
|
return state.delete(action.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
case STATUS_IMPORT:
|
case STATUS_IMPORT:
|
||||||
return importStatus(state, action.status);
|
return importStatus(state, action.status);
|
||||||
case STATUSES_IMPORT:
|
case STATUSES_IMPORT:
|
||||||
|
|||||||
@@ -1888,7 +1888,7 @@ a.sparkline {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|
||||||
li {
|
> li {
|
||||||
counter-increment: step 1;
|
counter-increment: step 1;
|
||||||
padding-inline-start: 2.5rem;
|
padding-inline-start: 2.5rem;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
|||||||
@@ -2333,6 +2333,7 @@ a .account__avatar {
|
|||||||
.detailed-status__display-name,
|
.detailed-status__display-name,
|
||||||
.detailed-status__datetime,
|
.detailed-status__datetime,
|
||||||
.detailed-status__application,
|
.detailed-status__application,
|
||||||
|
.detailed-status__link,
|
||||||
.account__display-name {
|
.account__display-name {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
@@ -2365,7 +2366,8 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__application,
|
.detailed-status__application,
|
||||||
.detailed-status__datetime {
|
.detailed-status__datetime,
|
||||||
|
.detailed-status__link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2551,8 +2553,9 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status__relative-time,
|
.status__relative-time,
|
||||||
.detailed-status__datetime {
|
.detailed-status__datetime,
|
||||||
&:hover {
|
.detailed-status__link {
|
||||||
|
&:is(a):hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.editable {
|
&.editable,
|
||||||
|
&.disabled {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -159,7 +160,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__option.editable &__input {
|
&__option.editable &__input,
|
||||||
|
&__option.disabled &__input {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||||||
|
|
||||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||||
@quote.reject!
|
@quote.reject!
|
||||||
|
DistributionWorker.perform_async(@quote.status_id, { 'update' => true })
|
||||||
end
|
end
|
||||||
|
|
||||||
def forwarder
|
def forwarder
|
||||||
|
|||||||
@@ -57,8 +57,16 @@ class Antispam
|
|||||||
end
|
end
|
||||||
|
|
||||||
def report_if_needed!(account)
|
def report_if_needed!(account)
|
||||||
return if Report.unresolved.exists?(account: Account.representative, target_account: account)
|
return if system_reports.unresolved.exists?(target_account: account)
|
||||||
|
|
||||||
Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL')
|
system_reports.create!(
|
||||||
|
category: :spam,
|
||||||
|
comment: 'Account automatically reported for posting a banned URL',
|
||||||
|
target_account: account
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_reports
|
||||||
|
Account.representative.reports
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class Account < ApplicationRecord
|
|||||||
|
|
||||||
# Local user validations
|
# Local user validations
|
||||||
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
|
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
|
||||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
|
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? && !user&.bypass_registration_checks }
|
||||||
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
|
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
|
||||||
validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
|
validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
|
||||||
validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
|
validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
|
||||||
|
|||||||
@@ -44,12 +44,36 @@ class StatusEdit < ApplicationRecord
|
|||||||
scope :ordered, -> { order(id: :asc) }
|
scope :ordered, -> { order(id: :asc) }
|
||||||
|
|
||||||
delegate :local?, :application, :edited?, :edited_at,
|
delegate :local?, :application, :edited?, :edited_at,
|
||||||
:discarded?, :visibility, :language, to: :status
|
:discarded?, :reply?, :visibility, :language, to: :status
|
||||||
|
|
||||||
|
def with_media?
|
||||||
|
ordered_media_attachments.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_poll?
|
||||||
|
poll_options.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def poll
|
||||||
|
return @poll if defined?(@poll)
|
||||||
|
return @poll = nil if poll_options.blank?
|
||||||
|
|
||||||
|
@poll = Poll.new({
|
||||||
|
options: poll_options,
|
||||||
|
account_id: account_id,
|
||||||
|
status_id: status_id,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
alias preloadable_poll poll
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
return @emojis if defined?(@emojis)
|
return @emojis if defined?(@emojis)
|
||||||
|
|
||||||
@emojis = CustomEmoji.from_text([spoiler_text, text].join(' '), status.account.domain)
|
fields = [spoiler_text, text]
|
||||||
|
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||||
|
|
||||||
|
@emojis = CustomEmoji.from_text(fields.join(' '), status.account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ordered_media_attachments
|
def ordered_media_attachments
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ class User < ApplicationRecord
|
|||||||
delegate :can?, to: :role
|
delegate :can?, to: :role
|
||||||
|
|
||||||
attr_reader :invite_code, :date_of_birth
|
attr_reader :invite_code, :date_of_birth
|
||||||
attr_writer :external, :bypass_registration_checks, :current_account
|
attr_writer :external, :current_account
|
||||||
|
|
||||||
|
attribute :bypass_registration_checks, :boolean, default: false
|
||||||
|
|
||||||
def self.those_who_can(*any_of_privileges)
|
def self.those_who_can(*any_of_privileges)
|
||||||
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
||||||
@@ -505,10 +507,6 @@ class User < ApplicationRecord
|
|||||||
!!@external
|
!!@external
|
||||||
end
|
end
|
||||||
|
|
||||||
def bypass_registration_checks?
|
|
||||||
@bypass_registration_checks
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize_role
|
def sanitize_role
|
||||||
self.role = nil if role.present? && role.everyone?
|
self.role = nil if role.present? && role.everyone?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_interaction_policies!
|
def update_interaction_policies!
|
||||||
@status.quote_approval_policy = @status_parser.quote_policy
|
@status.update(quote_approval_policy: @status_parser.quote_policy)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_media_attachments!
|
def update_media_attachments!
|
||||||
@@ -112,6 +112,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
@status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)
|
@status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)
|
||||||
|
|
||||||
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
|
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
|
||||||
|
|
||||||
|
@status.media_attachments.reload if @media_attachments_changed
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_media_files!
|
def download_media_files!
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class BackupService < BaseService
|
|||||||
def build_archive!
|
def build_archive!
|
||||||
tmp_file = Tempfile.new(%w(archive .zip))
|
tmp_file = Tempfile.new(%w(archive .zip))
|
||||||
|
|
||||||
|
Zip.write_zip64_support = true
|
||||||
Zip::File.open(tmp_file, create: true) do |zipfile|
|
Zip::File.open(tmp_file, create: true) do |zipfile|
|
||||||
dump_outbox!(zipfile)
|
dump_outbox!(zipfile)
|
||||||
dump_media_attachments!(zipfile)
|
dump_media_attachments!(zipfile)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
- if status.ordered_media_attachments.first.video?
|
|
||||||
= render_video_component(status, visible: false)
|
|
||||||
- elsif status.ordered_media_attachments.first.audio?
|
|
||||||
= render_audio_component(status)
|
|
||||||
- else
|
|
||||||
= render_media_gallery_component(status, visible: false)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
.batch-table__row
|
|
||||||
%label.batch-table__row__select.batch-checkbox
|
|
||||||
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
|
||||||
.batch-table__row__content
|
|
||||||
.status__card
|
|
||||||
- if status.reblog?
|
|
||||||
.status__prepend
|
|
||||||
= material_symbol('repeat')
|
|
||||||
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
|
|
||||||
- elsif status.reply? && status.in_reply_to_id.present?
|
|
||||||
.status__prepend
|
|
||||||
= material_symbol('reply')
|
|
||||||
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
|
|
||||||
.status__content><
|
|
||||||
- if status.proper.spoiler_text.blank?
|
|
||||||
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
|
|
||||||
- else
|
|
||||||
%details<
|
|
||||||
%summary><
|
|
||||||
%strong> Content warning: #{prerender_custom_emojis(h(status.proper.spoiler_text), status.proper.emojis)}
|
|
||||||
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
|
|
||||||
|
|
||||||
- unless status.proper.ordered_media_attachments.empty?
|
|
||||||
= render partial: 'admin/reports/media_attachments', locals: { status: status.proper }
|
|
||||||
|
|
||||||
.detailed-status__meta
|
|
||||||
- if status.application
|
|
||||||
= status.application.name
|
|
||||||
·
|
|
||||||
|
|
||||||
= link_to admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
|
|
||||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
|
||||||
- if status.edited?
|
|
||||||
·
|
|
||||||
= link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')),
|
|
||||||
admin_account_status_path(status.account_id, status),
|
|
||||||
class: 'detailed-status__datetime'
|
|
||||||
- if status.discarded?
|
|
||||||
·
|
|
||||||
%span.negative-hint= t('admin.statuses.deleted')
|
|
||||||
·
|
|
||||||
|
|
||||||
= material_symbol visibility_icon(status)
|
|
||||||
= t("statuses.visibilities.#{status.visibility}")
|
|
||||||
·
|
|
||||||
|
|
||||||
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', rel: 'noopener' do
|
|
||||||
= t('admin.statuses.view_publicly')
|
|
||||||
|
|
||||||
- if status.proper.sensitive?
|
|
||||||
·
|
|
||||||
= material_symbol('visibility_off')
|
|
||||||
= t('stream_entries.sensitive_content')
|
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
- if @statuses.empty?
|
- if @statuses.empty?
|
||||||
= nothing_here 'nothing-here--under-tabs'
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
- else
|
- else
|
||||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
|
||||||
|
|
||||||
- if @report.unresolved?
|
- if @report.unresolved?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|||||||
40
app/views/admin/shared/_status.html.haml
Normal file
40
app/views/admin/shared/_status.html.haml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
-# locals: (status:)
|
||||||
|
|
||||||
|
.status__card><
|
||||||
|
- if status.reblog?
|
||||||
|
.status__prepend
|
||||||
|
= material_symbol('repeat')
|
||||||
|
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
|
||||||
|
- elsif status.reply? && status.in_reply_to_id.present?
|
||||||
|
.status__prepend
|
||||||
|
= material_symbol('reply')
|
||||||
|
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
|
||||||
|
|
||||||
|
= render partial: 'admin/shared/status_content', locals: { status: status.proper }
|
||||||
|
|
||||||
|
.detailed-status__meta
|
||||||
|
- if status.application
|
||||||
|
= status.application.name
|
||||||
|
·
|
||||||
|
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
|
||||||
|
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }><= l(status.created_at)
|
||||||
|
- if status.edited?
|
||||||
|
·
|
||||||
|
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status, { anchor: 'history' }), class: 'detailed-status__datetime' do
|
||||||
|
%span><= t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'relative-formatted'))
|
||||||
|
- if status.discarded?
|
||||||
|
·
|
||||||
|
%span.negative-hint= t('admin.statuses.deleted')
|
||||||
|
- unless status.reblog?
|
||||||
|
·
|
||||||
|
%span<
|
||||||
|
= material_symbol(visibility_icon(status))
|
||||||
|
= t("statuses.visibilities.#{status.visibility}")
|
||||||
|
- if status.proper.sensitive?
|
||||||
|
·
|
||||||
|
= material_symbol('visibility_off')
|
||||||
|
= t('stream_entries.sensitive_content')
|
||||||
|
- unless status.direct_visibility?
|
||||||
|
·
|
||||||
|
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do
|
||||||
|
= t('admin.statuses.view_publicly')
|
||||||
22
app/views/admin/shared/_status_attachments.html.haml
Normal file
22
app/views/admin/shared/_status_attachments.html.haml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
- if status.with_poll?
|
||||||
|
.poll
|
||||||
|
%ul
|
||||||
|
- status.preloadable_poll.options.each_with_index do |option, _index|
|
||||||
|
%li
|
||||||
|
%label.poll__option.disabled<>
|
||||||
|
- if status.preloadable_poll.multiple?
|
||||||
|
%span.poll__input.checkbox{ role: 'checkbox', 'aria-label': option }
|
||||||
|
- else
|
||||||
|
%span.poll__input{ role: 'radio', 'aria-label': option }
|
||||||
|
%span.poll__option__text
|
||||||
|
= prerender_custom_emojis(html_aware_format(option, status.local?, multiline: false), status.emojis)
|
||||||
|
%button.button.button-secondary{ disabled: true }
|
||||||
|
= t('polls.vote')
|
||||||
|
|
||||||
|
- if status.with_media?
|
||||||
|
- if status.ordered_media_attachments.first.video?
|
||||||
|
= render_video_component(status, visible: false)
|
||||||
|
- elsif status.ordered_media_attachments.first.audio?
|
||||||
|
= render_audio_component(status)
|
||||||
|
- else
|
||||||
|
= render_media_gallery_component(status, visible: false)
|
||||||
5
app/views/admin/shared/_status_batch_row.html.haml
Normal file
5
app/views/admin/shared/_status_batch_row.html.haml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.batch-table__row
|
||||||
|
%label.batch-table__row__select.batch-checkbox
|
||||||
|
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
||||||
|
.batch-table__row__content
|
||||||
|
= render partial: 'admin/shared/status', object: status
|
||||||
10
app/views/admin/shared/_status_content.html.haml
Normal file
10
app/views/admin/shared/_status_content.html.haml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.status__content><
|
||||||
|
- if status.spoiler_text.present?
|
||||||
|
%details<
|
||||||
|
%summary><
|
||||||
|
%strong> Content warning: #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||||
|
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||||
|
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }
|
||||||
|
- else
|
||||||
|
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||||
|
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }
|
||||||
@@ -9,17 +9,7 @@
|
|||||||
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
||||||
|
|
||||||
.status
|
.status
|
||||||
.status__content><
|
= render partial: 'admin/shared/status_content', locals: { status: status_edit }
|
||||||
- if status_edit.spoiler_text.blank?
|
|
||||||
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
|
|
||||||
- else
|
|
||||||
%details<
|
|
||||||
%summary><
|
|
||||||
%strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)}
|
|
||||||
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
|
|
||||||
|
|
||||||
- unless status_edit.ordered_media_attachments.empty?
|
|
||||||
= render partial: 'admin/reports/media_attachments', locals: { status: status_edit }
|
|
||||||
|
|
||||||
.detailed-status__meta
|
.detailed-status__meta
|
||||||
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
||||||
|
|||||||
@@ -47,6 +47,6 @@
|
|||||||
- if @statuses.empty?
|
- if @statuses.empty?
|
||||||
= nothing_here 'nothing-here--under-tabs'
|
= nothing_here 'nothing-here--under-tabs'
|
||||||
- else
|
- else
|
||||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
|
||||||
|
|
||||||
= paginate @statuses
|
= paginate @statuses
|
||||||
|
|||||||
@@ -53,52 +53,11 @@
|
|||||||
|
|
||||||
%h3= t('admin.statuses.contents')
|
%h3= t('admin.statuses.contents')
|
||||||
|
|
||||||
.status__card
|
= render partial: 'admin/shared/status', object: @status
|
||||||
- if @status.reblog?
|
|
||||||
.status__prepend
|
|
||||||
= material_symbol('repeat')
|
|
||||||
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(@status.proper.account, path: admin_account_status_path(@status.proper.account.id, @status.proper.id)))
|
|
||||||
- elsif @status.reply? && @status.in_reply_to_id.present?
|
|
||||||
.status__prepend
|
|
||||||
= material_symbol('reply')
|
|
||||||
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(@status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil))
|
|
||||||
.status__content><
|
|
||||||
- if @status.proper.spoiler_text.blank?
|
|
||||||
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
|
|
||||||
- else
|
|
||||||
%details<
|
|
||||||
%summary><
|
|
||||||
%strong> Content warning: #{prerender_custom_emojis(h(@status.proper.spoiler_text), @status.proper.emojis)}
|
|
||||||
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
|
|
||||||
|
|
||||||
- unless @status.proper.ordered_media_attachments.empty?
|
|
||||||
= render partial: 'admin/reports/media_attachments', locals: { status: @status.proper }
|
|
||||||
|
|
||||||
.detailed-status__meta
|
|
||||||
- if @status.application
|
|
||||||
= @status.application.name
|
|
||||||
·
|
|
||||||
%span.detailed-status__datetime
|
|
||||||
%time.formatted{ datetime: @status.created_at.iso8601, title: l(@status.created_at) }= l(@status.created_at)
|
|
||||||
- if @status.edited?
|
|
||||||
·
|
|
||||||
%span.detailed-status__datetime
|
|
||||||
= t('statuses.edited_at_html', date: content_tag(:time, l(@status.edited_at), datetime: @status.edited_at.iso8601, title: l(@status.edited_at), class: 'formatted'))
|
|
||||||
- if @status.discarded?
|
|
||||||
·
|
|
||||||
%span.negative-hint= t('admin.statuses.deleted')
|
|
||||||
- unless @status.reblog?
|
|
||||||
·
|
|
||||||
= material_symbol(visibility_icon(@status))
|
|
||||||
= t("statuses.visibilities.#{@status.visibility}")
|
|
||||||
- if @status.proper.sensitive?
|
|
||||||
·
|
|
||||||
= material_symbol('visibility_off')
|
|
||||||
= t('stream_entries.sensitive_content')
|
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
%h3= t('admin.statuses.history')
|
%h3#history= t('admin.statuses.history')
|
||||||
- if @status.edits.empty?
|
- if @status.edits.empty?
|
||||||
%p= t('admin.statuses.no_history')
|
%p= t('admin.statuses.no_history')
|
||||||
- else
|
- else
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
= vite_react_refresh_tag
|
= vite_react_refresh_tag
|
||||||
= vite_polyfills_tag
|
= vite_polyfills_tag
|
||||||
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
||||||
= vite_stylesheet_tag 'styles/entrypoints/inert.scss', media: 'all', id: 'inert-style' # TODO: flavour
|
= vite_stylesheet_tag 'styles/entrypoints/inert.scss', media: 'all', id: 'inert-style', crossorigin: 'anonymous' # TODO: flavour
|
||||||
= flavoured_vite_typescript_tag 'common.ts', crossorigin: 'anonymous'
|
= flavoured_vite_typescript_tag 'common.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
= vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" # TODO: fix preload for flavour
|
= vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" # TODO: fix preload for flavour
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ class Scheduler::SelfDestructScheduler
|
|||||||
|
|
||||||
def sidekiq_overwhelmed?
|
def sidekiq_overwhelmed?
|
||||||
redis_mem_info = Sidekiq.default_configuration.redis_info
|
redis_mem_info = Sidekiq.default_configuration.redis_info
|
||||||
|
maxmemory = [redis_mem_info['maxmemory'].to_f, redis_mem_info['total_system_memory'].to_f].filter(&:positive?).min
|
||||||
|
|
||||||
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > redis_mem_info['total_system_memory'].to_f * MAX_REDIS_MEM_USAGE
|
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > maxmemory * MAX_REDIS_MEM_USAGE
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_accounts!
|
def delete_accounts!
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Disable httplog in production unless log_level is `debug`
|
# Disable in production unless log level is `debug`
|
||||||
if !Rails.env.production? || Rails.configuration.log_level == :debug
|
if Rails.env.local? || Rails.logger.debug?
|
||||||
require 'httplog'
|
require 'httplog'
|
||||||
|
|
||||||
HttpLog.configure do |config|
|
HttpLog.configure do |config|
|
||||||
|
|||||||
@@ -1707,6 +1707,7 @@ en:
|
|||||||
self_vote: You cannot vote in your own polls
|
self_vote: You cannot vote in your own polls
|
||||||
too_few_options: must have more than one item
|
too_few_options: must have more than one item
|
||||||
too_many_options: can't contain more than %{max} items
|
too_many_options: can't contain more than %{max} items
|
||||||
|
vote: Vote
|
||||||
preferences:
|
preferences:
|
||||||
other: Other
|
other: Other
|
||||||
posting_defaults: Posting defaults
|
posting_defaults: Posting defaults
|
||||||
@@ -1876,12 +1877,12 @@ en:
|
|||||||
ownership: Someone else's post cannot be pinned
|
ownership: Someone else's post cannot be pinned
|
||||||
reblog: A boost cannot be pinned
|
reblog: A boost cannot be pinned
|
||||||
quote_policies:
|
quote_policies:
|
||||||
followers: Only your followers
|
followers: Followers only
|
||||||
nobody: Nobody
|
nobody: Just me
|
||||||
public: Everyone
|
public: Anyone
|
||||||
title: '%{name}: "%{quote}"'
|
title: '%{name}: "%{quote}"'
|
||||||
visibilities:
|
visibilities:
|
||||||
direct: Direct
|
direct: Private mention
|
||||||
private: Followers-only
|
private: Followers-only
|
||||||
private_long: Only show to followers
|
private_long: Only show to followers
|
||||||
public: Public
|
public: Public
|
||||||
|
|||||||
@@ -363,10 +363,6 @@ namespace :api, format: false do
|
|||||||
namespace :web do
|
namespace :web do
|
||||||
resource :settings, only: [:update]
|
resource :settings, only: [:update]
|
||||||
resources :embeds, only: [:show]
|
resources :embeds, only: [:show]
|
||||||
resources :push_subscriptions, only: [:create, :destroy] do
|
resources :push_subscriptions, only: [:create, :destroy, :update]
|
||||||
member do
|
|
||||||
put :update
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,27 +12,22 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||||||
id: 'foo',
|
id: 'foo',
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
object: object_json,
|
||||||
signature: 'foo',
|
signature: 'foo',
|
||||||
}.with_indifferent_access
|
}.deep_stringify_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:object_json) { ActivityPub::TagManager.instance.uri_for(status) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
|
||||||
subject.perform
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'deletes sender\'s status' do
|
it 'deletes sender\'s status' do
|
||||||
|
subject.perform
|
||||||
expect(Status.find_by(id: status.id)).to be_nil
|
expect(Status.find_by(id: status.id)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the status has been reblogged' do
|
context 'when the status has been reblogged' do
|
||||||
describe '#perform' do
|
|
||||||
subject { described_class.new(json, sender) }
|
|
||||||
|
|
||||||
let!(:reblogger) { Fabricate(:account) }
|
let!(:reblogger) { Fabricate(:account) }
|
||||||
let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||||
let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) }
|
let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) }
|
||||||
@@ -55,12 +50,8 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||||||
expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the status has been reported' do
|
context 'when the status has been reported' do
|
||||||
describe '#perform' do
|
|
||||||
subject { described_class.new(json, sender) }
|
|
||||||
|
|
||||||
let!(:reporter) { Fabricate(:account) }
|
let!(:reporter) { Fabricate(:account) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@@ -76,23 +67,9 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||||||
expect(Status.with_discarded.find_by(id: status.id)).to_not be_nil
|
expect(Status.with_discarded.find_by(id: status.id)).to_not be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the deleted object is an account' do
|
context 'when the deleted object is an account' do
|
||||||
let(:json) do
|
let(:object_json) { ActivityPub::TagManager.instance.uri_for(sender) }
|
||||||
{
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
id: 'foo',
|
|
||||||
type: 'Delete',
|
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
signature: 'foo',
|
|
||||||
}.with_indifferent_access
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#perform' do
|
|
||||||
subject { described_class.new(json, sender) }
|
|
||||||
|
|
||||||
let(:service) { instance_double(DeleteAccountService, call: true) }
|
let(:service) { instance_double(DeleteAccountService, call: true) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@@ -106,7 +83,6 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||||||
.to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true })
|
.to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the deleted object is a quote authorization' do
|
context 'when the deleted object is a quote authorization' do
|
||||||
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||||
@@ -114,19 +90,29 @@ RSpec.describe ActivityPub::Activity::Delete do
|
|||||||
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||||
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||||
|
|
||||||
let(:json) do
|
let(:object_json) { quote.approval_uri }
|
||||||
{
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
it 'revokes the authorization' do
|
||||||
id: 'foo',
|
expect { subject.perform }
|
||||||
type: 'Delete',
|
.to change { quote.reload.state }.to('revoked')
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
end
|
||||||
object: quote.approval_uri,
|
|
||||||
signature: 'foo',
|
|
||||||
}.with_indifferent_access
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
context 'when the deleted object is an inlined quote authorization' do
|
||||||
subject { described_class.new(json, sender) }
|
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||||
|
let(:status) { Fabricate(:status, account: quoter) }
|
||||||
|
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||||
|
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
type: 'QuoteAuthorization',
|
||||||
|
id: quote.approval_uri,
|
||||||
|
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||||
|
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||||
|
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||||
|
}.deep_stringify_keys
|
||||||
|
end
|
||||||
|
|
||||||
it 'revokes the authorization' do
|
it 'revokes the authorization' do
|
||||||
expect { subject.perform }
|
expect { subject.perform }
|
||||||
|
|||||||
47
spec/lib/antispam_spec.rb
Normal file
47
spec/lib/antispam_spec.rb
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Antispam do
|
||||||
|
describe '#local_preflight_check!' do
|
||||||
|
subject { described_class.new.local_preflight_check!(status) }
|
||||||
|
|
||||||
|
let(:status) { Fabricate :status }
|
||||||
|
|
||||||
|
context 'when there is no spammy text registered' do
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with spammy text' do
|
||||||
|
before { redis.sadd 'antispam:spammy_texts', 'https://banned.example' }
|
||||||
|
|
||||||
|
context 'when status matches' do
|
||||||
|
let(:status) { Fabricate :status, text: 'I use https://banned.example urls in my text' }
|
||||||
|
|
||||||
|
it 'raises error and reports' do
|
||||||
|
expect { subject }
|
||||||
|
.to raise_error(described_class::SilentlyDrop)
|
||||||
|
.and change(spam_reports, :count).by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when report already exists' do
|
||||||
|
before { Fabricate :report, account: Account.representative, target_account: status.account }
|
||||||
|
|
||||||
|
it 'raises error and does not report' do
|
||||||
|
expect { subject }
|
||||||
|
.to raise_error(described_class::SilentlyDrop)
|
||||||
|
.and not_change(spam_reports, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def spam_reports
|
||||||
|
Account.representative.reports.where(target_account: status.account).spam
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when status does not match' do
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -32,6 +32,7 @@ RSpec.describe Mastodon::CLI::Accounts do
|
|||||||
|
|
||||||
describe '#create' do
|
describe '#create' do
|
||||||
let(:action) { :create }
|
let(:action) { :create }
|
||||||
|
let(:username) { 'tootctl_username' }
|
||||||
|
|
||||||
shared_examples 'a new user with given email address and username' do
|
shared_examples 'a new user with given email address and username' do
|
||||||
it 'creates user and accounts from options and displays success message' do
|
it 'creates user and accounts from options and displays success message' do
|
||||||
@@ -48,18 +49,24 @@ RSpec.describe Mastodon::CLI::Accounts do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_from_options
|
def account_from_options
|
||||||
Account.find_local('tootctl_username')
|
Account.find_local(username)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when required USERNAME and --email are provided' do
|
context 'when required USERNAME and --email are provided' do
|
||||||
let(:arguments) { ['tootctl_username'] }
|
let(:arguments) { [username] }
|
||||||
|
|
||||||
context 'with USERNAME and --email only' do
|
context 'with USERNAME and --email only' do
|
||||||
let(:options) { { email: 'tootctl@example.com' } }
|
let(:options) { { email: 'tootctl@example.com' } }
|
||||||
|
|
||||||
it_behaves_like 'a new user with given email address and username'
|
it_behaves_like 'a new user with given email address and username'
|
||||||
|
|
||||||
|
context 'with a reserved username' do
|
||||||
|
let(:username) { 'security' }
|
||||||
|
|
||||||
|
it_behaves_like 'a new user with given email address and username'
|
||||||
|
end
|
||||||
|
|
||||||
context 'with invalid --email value' do
|
context 'with invalid --email value' do
|
||||||
let(:options) { { email: 'invalid' } }
|
let(:options) { { email: 'invalid' } }
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ RSpec.describe 'API Web Push Subscriptions' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /api/web/push_subscriptions' do
|
describe 'PUT /api/web/push_subscriptions/:id' do
|
||||||
before { sign_in Fabricate :user }
|
before { sign_in Fabricate :user }
|
||||||
|
|
||||||
let(:subscription) { Fabricate :web_push_subscription }
|
let(:subscription) { Fabricate :web_push_subscription }
|
||||||
|
|||||||
@@ -316,6 +316,23 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
|||||||
expect(existing_status.edits).to_not be_empty
|
expect(existing_status.edits).to_not be_empty
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an implicit update to quoting policy' do
|
||||||
|
let(:object) do
|
||||||
|
note.merge({
|
||||||
|
'content' => existing_status.text,
|
||||||
|
'interactionPolicy' => {
|
||||||
|
'canQuote' => {
|
||||||
|
'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates status' do
|
||||||
|
expect(existing_status.reload.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,42 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when originally without media attachments and text is removed' do
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png'))
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
content: '',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
attachment: [
|
||||||
|
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates media attachments, fetches attachment, records media and text removal in edit' do
|
||||||
|
subject.call(status, json, json)
|
||||||
|
|
||||||
|
expect(status.reload.ordered_media_attachments.first)
|
||||||
|
.to be_present
|
||||||
|
.and(have_attributes(remote_url: 'https://example.com/foo.png'))
|
||||||
|
|
||||||
|
expect(a_request(:get, 'https://example.com/foo.png'))
|
||||||
|
.to have_been_made
|
||||||
|
|
||||||
|
expect(status.edits.reload.last.ordered_media_attachment_ids)
|
||||||
|
.to_not be_empty
|
||||||
|
|
||||||
|
expect(status.edits.reload.last.text)
|
||||||
|
.to_not be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when originally with media attachments' do
|
context 'when originally with media attachments' do
|
||||||
let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
|
let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ RSpec.describe 'Share page', :js, :streaming do
|
|||||||
fill_in_form
|
fill_in_form
|
||||||
|
|
||||||
expect(page)
|
expect(page)
|
||||||
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
|
.to have_current_path(%r{/@bob/[0-9]+})
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_in_form
|
def fill_in_form
|
||||||
|
|||||||
Reference in New Issue
Block a user