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 { 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_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||||
export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
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) => {
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
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)
|
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 } }) => {
|
.then(({ data: { reports } }) => {
|
||||||
reports.forEach(report => {
|
reports.forEach(report => {
|
||||||
dispatch(importFetchedAccount(report.account));
|
dispatch(importFetchedAccount(report.account));
|
||||||
|
@ -118,9 +144,27 @@ export function fetchReports(params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchReports(ids, state) {
|
function patchReports(ids, state) {
|
||||||
const reports = ids.map(id => ({ id, state }));
|
|
||||||
return (dispatch, getState) => {
|
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 });
|
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)
|
return api(getState)
|
||||||
.patch('/api/pleroma/admin/reports', { reports })
|
.patch('/api/pleroma/admin/reports', { reports })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -134,12 +178,45 @@ export function closeReports(ids) {
|
||||||
return patchReports(ids, 'closed');
|
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) => {
|
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 };
|
const params = { filters: filters.join(), page, page_size: pageSize };
|
||||||
if (query) params.query = query;
|
if (query) params.query = query;
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.get('/api/pleroma/admin/users', { params })
|
.get('/api/pleroma/admin/users', { params })
|
||||||
.then(({ data: { users, count, page_size: pageSize } }) => {
|
.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) => {
|
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 });
|
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)
|
return api(getState)
|
||||||
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
|
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
|
||||||
.then(({ data: { users } }) => {
|
.then(({ data: { users } }) => {
|
||||||
|
@ -182,8 +280,26 @@ export function deleteUsers(accountIds) {
|
||||||
|
|
||||||
export function approveUsers(accountIds) {
|
export function approveUsers(accountIds) {
|
||||||
return (dispatch, getState) => {
|
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 });
|
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)
|
return api(getState)
|
||||||
.patch('/api/pleroma/admin/users/approve', { nicknames })
|
.patch('/api/pleroma/admin/users/approve', { nicknames })
|
||||||
.then(({ data: { users } }) => {
|
.then(({ data: { users } }) => {
|
||||||
|
|
|
@ -14,8 +14,8 @@ import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import ReportStatus from './report_status';
|
import ReportStatus from './report_status';
|
||||||
|
|
||||||
import type { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
import type { Status } from 'soapbox/types/entities';
|
import type { Account, AdminReport, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' },
|
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' },
|
||||||
|
@ -24,7 +24,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IReport {
|
interface IReport {
|
||||||
report: ImmutableMap<string, any>;
|
report: AdminReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Report: React.FC<IReport> = ({ report }) => {
|
const Report: React.FC<IReport> = ({ report }) => {
|
||||||
|
@ -33,32 +33,35 @@ const Report: React.FC<IReport> = ({ report }) => {
|
||||||
|
|
||||||
const [accordionExpanded, setAccordionExpanded] = useState(false);
|
const [accordionExpanded, setAccordionExpanded] = useState(false);
|
||||||
|
|
||||||
|
const account = report.account as Account;
|
||||||
|
const targetAccount = report.target_account as Account;
|
||||||
|
|
||||||
const makeMenu = () => {
|
const makeMenu = () => {
|
||||||
return [{
|
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,
|
action: handleDeactivateUser,
|
||||||
icon: require('@tabler/icons/icons/user-off.svg'),
|
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,
|
action: handleDeleteUser,
|
||||||
icon: require('@tabler/icons/icons/user-minus.svg'),
|
icon: require('@tabler/icons/icons/user-minus.svg'),
|
||||||
}];
|
}];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseReport = () => {
|
const handleCloseReport = () => {
|
||||||
dispatch(closeReports([report.get('id')])).then(() => {
|
dispatch(closeReports([report.id])).then(() => {
|
||||||
const message = intl.formatMessage(messages.reportClosed, { name: report.getIn(['account', 'username']) as string });
|
const message = intl.formatMessage(messages.reportClosed, { name: targetAccount.username as string });
|
||||||
dispatch(snackbar.success(message));
|
dispatch(snackbar.success(message));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeactivateUser = () => {
|
const handleDeactivateUser = () => {
|
||||||
const accountId = report.getIn(['account', 'id']);
|
const accountId = targetAccount.id;
|
||||||
dispatch(deactivateUserModal(intl, accountId, () => handleCloseReport()));
|
dispatch(deactivateUserModal(intl, accountId, () => handleCloseReport()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteUser = () => {
|
const handleDeleteUser = () => {
|
||||||
const accountId = report.getIn(['account', 'id']) as string;
|
const accountId = targetAccount.id as string;
|
||||||
dispatch(deleteUserModal(intl, accountId, () => handleCloseReport()));
|
dispatch(deleteUserModal(intl, accountId, () => handleCloseReport()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,17 +70,17 @@ const Report: React.FC<IReport> = ({ report }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = makeMenu();
|
const menu = makeMenu();
|
||||||
const statuses = report.get('statuses') as ImmutableList<Status>;
|
const statuses = report.statuses as ImmutableList<Status>;
|
||||||
const statusCount = statuses.count();
|
const statusCount = statuses.count();
|
||||||
const acct = report.getIn(['account', 'acct']) as string;
|
const acct = targetAccount.acct as string;
|
||||||
const reporterAcct = report.getIn(['actor', 'acct']) as string;
|
const reporterAcct = account.acct as string;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='admin-report' key={report.get('id')}>
|
<div className='admin-report' key={report.id}>
|
||||||
<div className='admin-report__avatar'>
|
<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}>
|
<Link to={`/@${acct}`} title={acct}>
|
||||||
<Avatar account={report.get('account')} size={32} />
|
<Avatar account={targetAccount} size={32} />
|
||||||
</Link>
|
</Link>
|
||||||
</HoverRefWrapper>
|
</HoverRefWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +90,7 @@ const Report: React.FC<IReport> = ({ report }) => {
|
||||||
id='admin.reports.report_title'
|
id='admin.reports.report_title'
|
||||||
defaultMessage='Report on {acct}'
|
defaultMessage='Report on {acct}'
|
||||||
values={{ 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>
|
<Link to={`/@${acct}`} title={acct}>@{acct}</Link>
|
||||||
</HoverRefWrapper>
|
</HoverRefWrapper>
|
||||||
) }}
|
) }}
|
||||||
|
@ -105,12 +108,12 @@ const Report: React.FC<IReport> = ({ report }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='admin-report__quote'>
|
<div className='admin-report__quote'>
|
||||||
{report.get('content', '').length > 0 && (
|
{(report.comment || '').length > 0 && (
|
||||||
<blockquote className='md' dangerouslySetInnerHTML={{ __html: report.get('content') }} />
|
<blockquote className='md' dangerouslySetInnerHTML={{ __html: report.comment }} />
|
||||||
)}
|
)}
|
||||||
<span className='byline'>
|
<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>
|
<Link to={`/@${reporterAcct}`} title={reporterAcct}>@{reporterAcct}</Link>
|
||||||
</HoverRefWrapper>
|
</HoverRefWrapper>
|
||||||
</span>
|
</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 { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { AdminReport, Attachment, Status } from 'soapbox/types/entities';
|
||||||
import type { Status, Attachment } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
|
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
|
||||||
|
@ -20,7 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
interface IReportStatus {
|
interface IReportStatus {
|
||||||
status: Status,
|
status: Status,
|
||||||
report?: ImmutableMap<string, any>,
|
report?: AdminReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
|
const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
|
||||||
|
|
|
@ -26,6 +26,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const account = useAppSelector(state => getAccount(state, accountId));
|
const account = useAppSelector(state => getAccount(state, accountId));
|
||||||
|
const adminAccount = useAppSelector(state => state.admin.users.get(accountId));
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
|
@ -45,12 +46,11 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='unapproved-account'>
|
<div className='unapproved-account'>
|
||||||
<div className='unapproved-account__bio'>
|
<div className='unapproved-account__bio'>
|
||||||
<div className='unapproved-account__nickname'>@{account.get('acct')}</div>
|
<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>
|
||||||
<div className='unapproved-account__actions'>
|
<div className='unapproved-account__actions'>
|
||||||
<IconButton src={require('@tabler/icons/icons/check.svg')} onClick={handleApprove} />
|
<IconButton src={require('@tabler/icons/icons/check.svg')} onClick={handleApprove} />
|
||||||
|
|
|
@ -42,7 +42,7 @@ const Reports: React.FC = () => {
|
||||||
scrollKey='admin-reports'
|
scrollKey='admin-reports'
|
||||||
emptyMessage={intl.formatMessage(messages.emptyMessage)}
|
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>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,6 +34,7 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
page: 0,
|
page: 0,
|
||||||
query: '',
|
query: '',
|
||||||
|
nextLink: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
clearState = callback => {
|
clearState = callback => {
|
||||||
|
@ -45,11 +46,11 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchNextPage = () => {
|
fetchNextPage = () => {
|
||||||
const { filters, page, query, pageSize } = this.state;
|
const { filters, page, query, pageSize, nextLink } = this.state;
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
|
|
||||||
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize))
|
this.props.dispatch(fetchUsers(filters, nextPage, query, pageSize, nextLink))
|
||||||
.then(({ users, count }) => {
|
.then(({ users, count, next }) => {
|
||||||
const newIds = users.map(user => user.id);
|
const newIds = users.map(user => user.id);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -57,6 +58,7 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
accountIds: this.state.accountIds.union(newIds),
|
accountIds: this.state.accountIds.union(newIds),
|
||||||
total: count,
|
total: count,
|
||||||
page: nextPage,
|
page: nextPage,
|
||||||
|
nextLink: next,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
@ -97,7 +99,7 @@ class UserIndex extends ImmutablePureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
const { accountIds, isLoading } = this.state;
|
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();
|
const showLoading = isLoading && accountIds.isEmpty();
|
||||||
|
|
||||||
|
|
|
@ -455,7 +455,7 @@ const UI: React.FC = ({ children }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.staff) {
|
if (account.staff) {
|
||||||
dispatch(fetchReports({ state: 'open' }));
|
dispatch(fetchReports({ resolved: false }));
|
||||||
dispatch(fetchUsers(['local', 'need_approval']));
|
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 { AccountRecord, FieldRecord, normalizeAccount } from './account';
|
||||||
|
export { AdminAccountRecord, normalizeAdminAccount } from './admin_account';
|
||||||
|
export { AdminReportRecord, normalizeAdminReport } from './admin_report';
|
||||||
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
||||||
export { CardRecord, normalizeCard } from './card';
|
export { CardRecord, normalizeCard } from './card';
|
||||||
export { ChatRecord, normalizeChat } from './chat';
|
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 => {
|
const importAdminUsers = (state: State, adminUsers: Array<Record<string, any>>): State => {
|
||||||
return state.withMutations((state: State) => {
|
return state.withMutations((state: State) => {
|
||||||
adminUsers.forEach(adminUser => {
|
adminUsers.filter(adminUser => !adminUser.account).forEach(adminUser => {
|
||||||
importAdminUser(state, ImmutableMap(fromJS(adminUser)));
|
importAdminUser(state, ImmutableMap(fromJS(adminUser)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,15 +19,18 @@ import {
|
||||||
ADMIN_USERS_DELETE_SUCCESS,
|
ADMIN_USERS_DELETE_SUCCESS,
|
||||||
ADMIN_USERS_APPROVE_REQUEST,
|
ADMIN_USERS_APPROVE_REQUEST,
|
||||||
ADMIN_USERS_APPROVE_SUCCESS,
|
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 { AnyAction } from 'redux';
|
||||||
import type { Config } from 'soapbox/utils/config_db';
|
import type { Config } from 'soapbox/utils/config_db';
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
const ReducerRecord = ImmutableRecord({
|
||||||
reports: ImmutableMap<string, any>(),
|
reports: ImmutableMap<string, ReducerAdminReport>(),
|
||||||
openReports: ImmutableOrderedSet<string>(),
|
openReports: ImmutableOrderedSet<string>(),
|
||||||
users: ImmutableMap<string, any>(),
|
users: ImmutableMap<string, ReducerAdminAccount>(),
|
||||||
latestUsers: ImmutableOrderedSet<string>(),
|
latestUsers: ImmutableOrderedSet<string>(),
|
||||||
awaitingApproval: ImmutableOrderedSet<string>(),
|
awaitingApproval: ImmutableOrderedSet<string>(),
|
||||||
configs: ImmutableList<Config>(),
|
configs: ImmutableList<Config>(),
|
||||||
|
@ -36,6 +39,21 @@ const ReducerRecord = ImmutableRecord({
|
||||||
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
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?
|
// Umm... based?
|
||||||
// https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51
|
// https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51
|
||||||
type InnerRecord<R> = R extends ImmutableRecord<infer TProps> ? TProps : never;
|
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 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 SetKeys = keyof FilterConditionally<InnerState, ImmutableOrderedSet<string>>;
|
||||||
|
|
||||||
type APIReport = { id: string, state: string, statuses: any[] };
|
type APIReport = { id: string, state: string, statuses: any[] };
|
||||||
type APIUser = { id: string, email: string, nickname: string, registration_reason: string };
|
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 => (
|
const minifyUser = (user: AdminAccountRecord): ReducerAdminAccount => {
|
||||||
state.setIn(['users', user.id], ImmutableMap({
|
return user.mergeWith((o, n) => n || o, {
|
||||||
email: user.email,
|
account: normalizeId(user.getIn(['account', 'id'])),
|
||||||
registration_reason: user.registration_reason,
|
}) 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 {
|
function importUsers(state: State, users: APIUser[], filters: Filter[], page: number): State {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
|
@ -97,7 +119,8 @@ function importUsers(state: State, users: APIUser[], filters: Filter[], page: nu
|
||||||
maybeImportLatest(state, users, filters, page);
|
maybeImportLatest(state, users, filters, page);
|
||||||
|
|
||||||
users.forEach(user => {
|
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 {
|
function approveUsers(state: State, users: APIUser[]): State {
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
state.update('awaitingApproval', orderedSet => orderedSet.delete(user.nickname));
|
const normalizedUser = fixUser(user);
|
||||||
state.setIn(['users', user.nickname], fromJS(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 => {
|
return state.withMutations(state => {
|
||||||
reports.forEach(report => {
|
reports.forEach(report => {
|
||||||
report.statuses = report.statuses.map(status => status.id);
|
const normalizedReport = fixReport(report);
|
||||||
if (report.state === 'open') {
|
if (!normalizedReport.action_taken) {
|
||||||
state.update('openReports', orderedSet => orderedSet.add(report.id));
|
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(
|
return createSelector(
|
||||||
[
|
[
|
||||||
(state: RootState, id: string) => state.admin.reports.get(id),
|
(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)))
|
statusId => state.statuses.get(normalizeId(statusId)))
|
||||||
.filter((s: any) => s)
|
.filter((s: any) => s)
|
||||||
.map((s: any) => getStatus(state, s.toJS())),
|
.map((s: any) => getStatus(state, s.toJS())),
|
||||||
],
|
],
|
||||||
|
|
||||||
(report, statuses) => {
|
(report, account, targetAccount, statuses) => {
|
||||||
if (!report) return null;
|
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 {
|
import {
|
||||||
|
AdminAccountRecord,
|
||||||
|
AdminReportRecord,
|
||||||
AccountRecord,
|
AccountRecord,
|
||||||
AttachmentRecord,
|
AttachmentRecord,
|
||||||
CardRecord,
|
CardRecord,
|
||||||
|
@ -17,6 +19,8 @@ import {
|
||||||
|
|
||||||
import type { Record as ImmutableRecord } from 'immutable';
|
import type { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
type AdminAccount = ReturnType<typeof AdminAccountRecord>;
|
||||||
|
type AdminReport = ReturnType<typeof AdminReportRecord>;
|
||||||
type Attachment = ReturnType<typeof AttachmentRecord>;
|
type Attachment = ReturnType<typeof AttachmentRecord>;
|
||||||
type Card = ReturnType<typeof CardRecord>;
|
type Card = ReturnType<typeof CardRecord>;
|
||||||
type Chat = ReturnType<typeof ChatRecord>;
|
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>>;
|
type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableRecord.Factory<T>>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AdminAccount,
|
||||||
|
AdminReport,
|
||||||
Account,
|
Account,
|
||||||
Attachment,
|
Attachment,
|
||||||
Card,
|
Card,
|
||||||
|
|
|
@ -316,6 +316,20 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
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.
|
* Can upload media attachments to statuses.
|
||||||
* @see POST /api/v1/media
|
* @see POST /api/v1/media
|
||||||
|
|
Ładowanie…
Reference in New Issue