kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Mastodon admin API
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-develop-3zknud/deployments/43^2
rodzic
609c6196fb
commit
88b91bce3e
|
@ -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 } }) => {
|
||||
|
|
|
@ -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'>
|
||||
—
|
||||
<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>
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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)));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue