Mastodon admin API

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-develop-3zknud/deployments/43^2
marcin mikołajczak 2022-05-15 15:11:59 +02:00
rodzic 609c6196fb
commit 88b91bce3e
15 zmienionych plików z 361 dodań i 61 usunięć

Wyświetl plik

@ -1,7 +1,8 @@
import { fetchRelationships } from 'soapbox/actions/accounts';
import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer';
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
import api, { getLinks } from '../api';
export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
@ -99,11 +100,36 @@ export function updateConfig(configs) {
};
}
export function fetchReports(params) {
export function fetchReports(params = {}) {
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
if (features.mastodonAdminApi) {
return api(getState)
.get('/api/v1/admin/reports', { params })
.then(({ data: reports }) => {
reports.forEach(report => {
dispatch(importFetchedAccount(report.account?.account));
dispatch(importFetchedAccount(report.target_account?.account));
dispatch(importFetchedStatuses(report.statuses));
});
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
}).catch(error => {
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
});
}
const { resolved } = params;
return api(getState)
.get('/api/pleroma/admin/reports', { params })
.get('/api/pleroma/admin/reports', { params: {
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
} })
.then(({ data: { reports } }) => {
reports.forEach(report => {
dispatch(importFetchedAccount(report.account));
@ -118,9 +144,27 @@ export function fetchReports(params) {
}
function patchReports(ids, state) {
const reports = ids.map(id => ({ id, state }));
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
const reports = ids.map(id => ({ id, state }));
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
if (features.mastodonAdminApi) {
return Promise.all(ids.map(id => api(getState)
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
.then(({ data }) => {
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
}).catch(error => {
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
}),
));
}
return api(getState)
.patch('/api/pleroma/admin/reports', { reports })
.then(() => {
@ -134,12 +178,45 @@ export function closeReports(ids) {
return patchReports(ids, 'closed');
}
export function fetchUsers(filters = [], page = 1, query, pageSize = 50) {
export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) {
return (dispatch, getState) => {
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
if (features.mastodonAdminApi) {
const params = {
username: query,
};
if (filters.includes('local')) params.local = true;
if (filters.includes('active')) params.active = true;
if (filters.includes('need_approval')) params.pending = true;
return api(getState)
.get(next || '/api/v1/admin/accounts', { params })
.then(({ data: accounts, ...response }) => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
const count = next
? page * pageSize + 1
: (page - 1) * pageSize + accounts.length;
dispatch(importFetchedAccounts(accounts.map(({ account }) => account)));
dispatch(fetchRelationships(accounts.map(account => account.id)));
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false });
return { users: accounts, count, pageSize, next: next?.uri || false };
}).catch(error => {
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
});
}
const params = { filters: filters.join(), page, page_size: pageSize };
if (query) params.query = query;
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
return api(getState)
.get('/api/pleroma/admin/users', { params })
.then(({ data: { users, count, page_size: pageSize } }) => {
@ -152,10 +229,31 @@ export function fetchUsers(filters = [], page = 1, query, pageSize = 50) {
};
}
export function deactivateUsers(accountIds) {
export function deactivateUsers(accountIds, reportId) {
return (dispatch, getState) => {
const nicknames = nicknamesFromIds(getState, accountIds);
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds });
if (features.mastodonAdminApi) {
return Promise.all(accountIds.map(accountId => {
api(getState)
.post(`/api/v1/admin/accounts/${accountId}/action`, {
type: 'disable',
report_id: reportId,
})
.then(() => {
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
}).catch(error => {
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
});
}));
}
const nicknames = nicknamesFromIds(getState, accountIds);
return api(getState)
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
.then(({ data: { users } }) => {
@ -182,8 +280,26 @@ export function deleteUsers(accountIds) {
export function approveUsers(accountIds) {
return (dispatch, getState) => {
const nicknames = nicknamesFromIds(getState, accountIds);
const state = getState();
const instance = state.get('instance');
const features = getFeatures(instance);
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds });
if (features.mastodonAdminApi) {
return Promise.all(accountIds.map(accountId => {
api(getState)
.post(`/api/v1/admin/accounts/${accountId}/approve`)
.then(({ data: user }) => {
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] });
}).catch(error => {
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] });
});
}));
}
const nicknames = nicknamesFromIds(getState, accountIds);
return api(getState)
.patch('/api/pleroma/admin/users/approve', { nicknames })
.then(({ data: { users } }) => {

Wyświetl plik

@ -14,8 +14,8 @@ import { useAppDispatch } from 'soapbox/hooks';
import ReportStatus from './report_status';
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import type { Status } from 'soapbox/types/entities';
import type { List as ImmutableList } from 'immutable';
import type { Account, AdminReport, Status } from 'soapbox/types/entities';
const messages = defineMessages({
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' },
@ -24,7 +24,7 @@ const messages = defineMessages({
});
interface IReport {
report: ImmutableMap<string, any>;
report: AdminReport;
}
const Report: React.FC<IReport> = ({ report }) => {
@ -33,32 +33,35 @@ const Report: React.FC<IReport> = ({ report }) => {
const [accordionExpanded, setAccordionExpanded] = useState(false);
const account = report.account as Account;
const targetAccount = report.target_account as Account;
const makeMenu = () => {
return [{
text: intl.formatMessage(messages.deactivateUser, { name: report.getIn(['account', 'username']) as string }),
text: intl.formatMessage(messages.deactivateUser, { name: targetAccount.username as string }),
action: handleDeactivateUser,
icon: require('@tabler/icons/icons/user-off.svg'),
}, {
text: intl.formatMessage(messages.deleteUser, { name: report.getIn(['account', 'username']) as string }),
text: intl.formatMessage(messages.deleteUser, { name: targetAccount.username as string }),
action: handleDeleteUser,
icon: require('@tabler/icons/icons/user-minus.svg'),
}];
};
const handleCloseReport = () => {
dispatch(closeReports([report.get('id')])).then(() => {
const message = intl.formatMessage(messages.reportClosed, { name: report.getIn(['account', 'username']) as string });
dispatch(closeReports([report.id])).then(() => {
const message = intl.formatMessage(messages.reportClosed, { name: targetAccount.username as string });
dispatch(snackbar.success(message));
}).catch(() => {});
};
const handleDeactivateUser = () => {
const accountId = report.getIn(['account', 'id']);
const accountId = targetAccount.id;
dispatch(deactivateUserModal(intl, accountId, () => handleCloseReport()));
};
const handleDeleteUser = () => {
const accountId = report.getIn(['account', 'id']) as string;
const accountId = targetAccount.id as string;
dispatch(deleteUserModal(intl, accountId, () => handleCloseReport()));
};
@ -67,17 +70,17 @@ const Report: React.FC<IReport> = ({ report }) => {
};
const menu = makeMenu();
const statuses = report.get('statuses') as ImmutableList<Status>;
const statuses = report.statuses as ImmutableList<Status>;
const statusCount = statuses.count();
const acct = report.getIn(['account', 'acct']) as string;
const reporterAcct = report.getIn(['actor', 'acct']) as string;
const acct = targetAccount.acct as string;
const reporterAcct = account.acct as string;
return (
<div className='admin-report' key={report.get('id')}>
<div className='admin-report' key={report.id}>
<div className='admin-report__avatar'>
<HoverRefWrapper accountId={report.getIn(['account', 'id']) as string} inline>
<HoverRefWrapper accountId={targetAccount.id as string} inline>
<Link to={`/@${acct}`} title={acct}>
<Avatar account={report.get('account')} size={32} />
<Avatar account={targetAccount} size={32} />
</Link>
</HoverRefWrapper>
</div>
@ -87,7 +90,7 @@ const Report: React.FC<IReport> = ({ report }) => {
id='admin.reports.report_title'
defaultMessage='Report on {acct}'
values={{ acct: (
<HoverRefWrapper accountId={report.getIn(['account', 'id']) as string} inline>
<HoverRefWrapper accountId={account.id as string} inline>
<Link to={`/@${acct}`} title={acct}>@{acct}</Link>
</HoverRefWrapper>
) }}
@ -105,12 +108,12 @@ const Report: React.FC<IReport> = ({ report }) => {
)}
</div>
<div className='admin-report__quote'>
{report.get('content', '').length > 0 && (
<blockquote className='md' dangerouslySetInnerHTML={{ __html: report.get('content') }} />
{(report.comment || '').length > 0 && (
<blockquote className='md' dangerouslySetInnerHTML={{ __html: report.comment }} />
)}
<span className='byline'>
&mdash;
<HoverRefWrapper accountId={report.getIn(['actor', 'id']) as string} inline>
<HoverRefWrapper accountId={account.id as string} inline>
<Link to={`/@${reporterAcct}`} title={reporterAcct}>@{reporterAcct}</Link>
</HoverRefWrapper>
</span>

Wyświetl plik

@ -10,8 +10,7 @@ import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch } from 'soapbox/hooks';
import type { Map as ImmutableMap } from 'immutable';
import type { Status, Attachment } from 'soapbox/types/entities';
import type { AdminReport, Attachment, Status } from 'soapbox/types/entities';
const messages = defineMessages({
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
@ -20,7 +19,7 @@ const messages = defineMessages({
interface IReportStatus {
status: Status,
report?: ImmutableMap<string, any>,
report?: AdminReport,
}
const ReportStatus: React.FC<IReportStatus> = ({ status }) => {

Wyświetl plik

@ -26,6 +26,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
const dispatch = useAppDispatch();
const account = useAppSelector(state => getAccount(state, accountId));
const adminAccount = useAppSelector(state => state.admin.users.get(accountId));
if (!account) return null;
@ -45,12 +46,11 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
}));
};
return (
<div className='unapproved-account'>
<div className='unapproved-account__bio'>
<div className='unapproved-account__nickname'>@{account.get('acct')}</div>
<blockquote className='md'>{account.pleroma.getIn(['admin', 'registration_reason'], '') as string}</blockquote>
<blockquote className='md'>{adminAccount?.invite_request || ''}</blockquote>
</div>
<div className='unapproved-account__actions'>
<IconButton src={require('@tabler/icons/icons/check.svg')} onClick={handleApprove} />

Wyświetl plik

@ -42,7 +42,7 @@ const Reports: React.FC = () => {
scrollKey='admin-reports'
emptyMessage={intl.formatMessage(messages.emptyMessage)}
>
{reports.map(report => <Report report={report} key={report.get('id')} />)}
{reports.map(report => report && <Report report={report} key={report?.id} />)}
</ScrollableList>
);
};

Wyświetl plik

@ -34,6 +34,7 @@ class UserIndex extends ImmutablePureComponent {
pageSize: 50,
page: 0,
query: '',
nextLink: undefined,
}
clearState = callback => {
@ -45,11 +46,11 @@ class UserIndex extends ImmutablePureComponent {
}
fetchNextPage = () => {
const { filters, page, query, pageSize } = this.state;
const { filters, page, query, pageSize, nextLink } = this.state;
const nextPage = page + 1;
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize))
.then(({ users, count }) => {
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize, nextLink))
.then(({ users, count, next }) => {
const newIds = users.map(user => user.id);
this.setState({
@ -57,6 +58,7 @@ class UserIndex extends ImmutablePureComponent {
accountIds: this.state.accountIds.union(newIds),
total: count,
page: nextPage,
nextLink: next,
});
})
.catch(() => {});
@ -97,7 +99,7 @@ class UserIndex extends ImmutablePureComponent {
render() {
const { intl } = this.props;
const { accountIds, isLoading } = this.state;
const hasMore = accountIds.count() < this.state.total;
const hasMore = accountIds.count() < this.state.total && this.state.nextLink !== false;
const showLoading = isLoading && accountIds.isEmpty();

Wyświetl plik

@ -455,7 +455,7 @@ const UI: React.FC = ({ children }) => {
}
if (account.staff) {
dispatch(fetchReports({ state: 'open' }));
dispatch(fetchReports({ resolved: false }));
dispatch(fetchUsers(['local', 'need_approval']));
}

Wyświetl plik

@ -0,0 +1,55 @@
/**
* Admin account normalizer:
* Converts API admin-level account information into our internal format.
*/
import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
fromJS,
} from 'immutable';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, EmbeddedEntity } from 'soapbox/types/entities';
export const AdminAccountRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account | ReducerAccount>,
approved: false,
confirmed: false,
created_at: new Date(),
disabled: false,
domain: '',
email: '',
id: '',
invite_request: null as string | null,
ip: null as string | null,
ips: ImmutableList<string>(),
locale: null as string | null,
role: null as 'admin' | 'moderator' | null,
sensitized: false,
silenced: false,
suspended: false,
username: '',
});
const normalizePleromaAccount = (account: ImmutableMap<string, any>) => {
if (!account.get('account')) {
return account.withMutations(account => {
account.set('approved', account.get('is_approved'));
account.set('confirmed', account.get('is_confirmed'));
account.set('disabled', !account.get('is_active'));
account.set('invite_request', account.get('registration_reason'));
account.set('role', account.getIn(['roles', 'admin']) ? 'admin' : (account.getIn(['roles', 'moderator']) ? 'moderator' : null));
});
}
return account;
};
export const normalizeAdminAccount = (account: Record<string, any>) => {
return AdminAccountRecord(
ImmutableMap(fromJS(account)).withMutations((account: ImmutableMap<string, any>) => {
normalizePleromaAccount(account);
}),
);
};

Wyświetl plik

@ -0,0 +1,51 @@
/**
* Admin report normalizer:
* Converts API admin-level report information into our internal format.
*/
import {
Map as ImmutableMap,
List as ImmutableList,
Record as ImmutableRecord,
fromJS,
} from 'immutable';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, EmbeddedEntity, Status } from 'soapbox/types/entities';
export const AdminReportRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account | ReducerAccount>,
action_taken: false,
action_taken_by_account: null as EmbeddedEntity<Account | ReducerAccount> | null,
assigned_account: null as EmbeddedEntity<Account | ReducerAccount> | null,
category: '',
comment: '',
created_at: new Date(),
id: '',
rules: ImmutableList<string>(),
statuses: ImmutableList<EmbeddedEntity<Status>>(),
target_account: null as EmbeddedEntity<Account | ReducerAccount>,
updated_at: new Date(),
});
const normalizePleromaReport = (report: ImmutableMap<string, any>) => {
if (report.get('actor')){
return report.withMutations(report => {
report.set('target_account', report.get('account'));
report.set('account', report.get('actor'));
report.set('action_taken', report.get('state') !== 'open');
report.set('comment', report.get('content'));
report.set('updated_at', report.get('created_at'));
});
}
return report;
};
export const normalizeAdminReport = (report: Record<string, any>) => {
return AdminReportRecord(
ImmutableMap(fromJS(report)).withMutations((report: ImmutableMap<string, any>) => {
normalizePleromaReport(report);
}),
);
};

Wyświetl plik

@ -1,4 +1,6 @@
export { AccountRecord, FieldRecord, normalizeAccount } from './account';
export { AdminAccountRecord, normalizeAdminAccount } from './admin_account';
export { AdminReportRecord, normalizeAdminReport } from './admin_report';
export { AttachmentRecord, normalizeAttachment } from './attachment';
export { CardRecord, normalizeCard } from './card';
export { ChatRecord, normalizeChat } from './chat';

Wyświetl plik

@ -228,7 +228,7 @@ const importAdminUser = (state: State, adminUser: ImmutableMap<string, any>): St
const importAdminUsers = (state: State, adminUsers: Array<Record<string, any>>): State => {
return state.withMutations((state: State) => {
adminUsers.forEach(adminUser => {
adminUsers.filter(adminUser => !adminUser.account).forEach(adminUser => {
importAdminUser(state, ImmutableMap(fromJS(adminUser)));
});
});

Wyświetl plik

@ -19,15 +19,18 @@ import {
ADMIN_USERS_DELETE_SUCCESS,
ADMIN_USERS_APPROVE_REQUEST,
ADMIN_USERS_APPROVE_SUCCESS,
} from '../actions/admin';
} from 'soapbox/actions/admin';
import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers';
import { APIEntity } from 'soapbox/types/entities';
import { normalizeId } from 'soapbox/utils/normalizers';
import type { AnyAction } from 'redux';
import type { Config } from 'soapbox/utils/config_db';
const ReducerRecord = ImmutableRecord({
reports: ImmutableMap<string, any>(),
reports: ImmutableMap<string, ReducerAdminReport>(),
openReports: ImmutableOrderedSet<string>(),
users: ImmutableMap<string, any>(),
users: ImmutableMap<string, ReducerAdminAccount>(),
latestUsers: ImmutableOrderedSet<string>(),
awaitingApproval: ImmutableOrderedSet<string>(),
configs: ImmutableList<Config>(),
@ -36,6 +39,21 @@ const ReducerRecord = ImmutableRecord({
type State = ReturnType<typeof ReducerRecord>;
type AdminAccountRecord = ReturnType<typeof normalizeAdminAccount>;
type AdminReportRecord = ReturnType<typeof normalizeAdminReport>;
export interface ReducerAdminAccount extends AdminAccountRecord {
account: string | null,
}
export interface ReducerAdminReport extends AdminReportRecord {
account: string | null,
target_account: string | null,
action_taken_by_account: string | null,
assigned_account: string | null,
statuses: ImmutableList<string | null>,
}
// Umm... based?
// https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51
type InnerRecord<R> = R extends ImmutableRecord<infer TProps> ? TProps : never;
@ -46,7 +64,6 @@ type InnerState = InnerRecord<State>;
type FilterConditionally<Source, Condition> = Pick<Source, {[K in keyof Source]: Source[K] extends Condition ? K : never}[keyof Source]>;
type SetKeys = keyof FilterConditionally<InnerState, ImmutableOrderedSet<string>>;
type APIReport = { id: string, state: string, statuses: any[] };
type APIUser = { id: string, email: string, nickname: string, registration_reason: string };
@ -84,12 +101,17 @@ const maybeImportLatest = (state: State, users: APIUser[], filters: Filter[], pa
}
};
const importUser = (state: State, user: APIUser): State => (
state.setIn(['users', user.id], ImmutableMap({
email: user.email,
registration_reason: user.registration_reason,
}))
);
const minifyUser = (user: AdminAccountRecord): ReducerAdminAccount => {
return user.mergeWith((o, n) => n || o, {
account: normalizeId(user.getIn(['account', 'id'])),
}) as ReducerAdminAccount;
};
const fixUser = (user: APIEntity): ReducerAdminAccount => {
return normalizeAdminAccount(user).withMutations(user => {
minifyUser(user);
}) as ReducerAdminAccount;
};
function importUsers(state: State, users: APIUser[], filters: Filter[], page: number): State {
return state.withMutations(state => {
@ -97,7 +119,8 @@ function importUsers(state: State, users: APIUser[], filters: Filter[], page: nu
maybeImportLatest(state, users, filters, page);
users.forEach(user => {
importUser(state, user);
const normalizedUser = fixUser(user);
state.setIn(['users', user.id], normalizedUser);
});
});
}
@ -114,20 +137,38 @@ function deleteUsers(state: State, accountIds: string[]): State {
function approveUsers(state: State, users: APIUser[]): State {
return state.withMutations(state => {
users.forEach(user => {
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.nickname));
state.setIn(['users', user.nickname], fromJS(user));
const normalizedUser = fixUser(user);
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.id));
state.setIn(['users', user.id], normalizedUser);
});
});
}
function importReports(state: State, reports: APIReport[]): State {
const minifyReport = (report: AdminReportRecord): ReducerAdminReport => {
return report.mergeWith((o, n) => n || o, {
account: normalizeId(report.getIn(['account', 'id'])),
target_account: normalizeId(report.getIn(['target_account', 'id'])),
action_taken_by_account: normalizeId(report.getIn(['action_taken_by_account', 'id'])),
assigned_account: normalizeId(report.getIn(['assigned_account', 'id'])),
statuses: report.get('statuses').map((status: any) => normalizeId(status.get('id'))),
}) as ReducerAdminReport;
};
const fixReport = (report: APIEntity): ReducerAdminReport => {
return normalizeAdminReport(report).withMutations(report => {
minifyReport(report);
}) as ReducerAdminReport;
};
function importReports(state: State, reports: APIEntity[]): State {
return state.withMutations(state => {
reports.forEach(report => {
report.statuses = report.statuses.map(status => status.id);
if (report.state === 'open') {
const normalizedReport = fixReport(report);
if (!normalizedReport.action_taken) {
state.update('openReports', orderedSet => orderedSet.add(report.id));
}
state.setIn(['reports', report.id], fromJS(report));
state.setIn(['reports', report.id], normalizedReport);
});
});
}

Wyświetl plik

@ -266,15 +266,26 @@ export const makeGetReport = () => {
return createSelector(
[
(state: RootState, id: string) => state.admin.reports.get(id),
(state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.getIn([id, 'statuses']))).map(
(state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.account || ''),
(state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.target_account || ''),
// (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.action_taken_by_account || ''),
// (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.assigned_account || ''),
(state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.get(id)?.statuses)).map(
statusId => state.statuses.get(normalizeId(statusId)))
.filter((s: any) => s)
.map((s: any) => getStatus(state, s.toJS())),
],
(report, statuses) => {
(report, account, targetAccount, statuses) => {
if (!report) return null;
return report.set('statuses', statuses);
return report.withMutations((report) => {
// @ts-ignore
report.set('account', account);
// @ts-ignore
report.set('target_account', targetAccount);
// @ts-ignore
report.set('statuses', statuses);
});
},
);
};

Wyświetl plik

@ -1,4 +1,6 @@
import {
AdminAccountRecord,
AdminReportRecord,
AccountRecord,
AttachmentRecord,
CardRecord,
@ -17,6 +19,8 @@ import {
import type { Record as ImmutableRecord } from 'immutable';
type AdminAccount = ReturnType<typeof AdminAccountRecord>;
type AdminReport = ReturnType<typeof AdminReportRecord>;
type Attachment = ReturnType<typeof AttachmentRecord>;
type Card = ReturnType<typeof CardRecord>;
type Chat = ReturnType<typeof ChatRecord>;
@ -47,6 +51,8 @@ type APIEntity = Record<string, any>;
type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableRecord.Factory<T>>;
export {
AdminAccount,
AdminReport,
Account,
Attachment,
Card,

Wyświetl plik

@ -316,6 +316,20 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA && gte(v.version, '0.9.9'),
]),
/**
* Can perform moderation actions with account and reports.
* @see {@link https://docs.joinmastodon.org/methods/admin/}
* @see GET /api/v1/admin/reports
* @see POST /api/v1/admin/reports/:report_id/resolve
* @see POST /api/v1/admin/reports/:report_id/reopen
* @see POST /api/v1/admin/accounts/:account_id/action
* @see POST /api/v1/admin/accounts/:account_id/approve
*/
mastodonAdminApi: any([
v.software === MASTODON && gte(v.compatVersion, '2.9.1'),
v.software === PLEROMA && v.build === SOAPBOX && gte(v.version, '2.4.50'),
]),
/**
* Can upload media attachments to statuses.
* @see POST /api/v1/media