mirror of
https://github.com/wassname/talk.git
synced 2026-08-16 11:29:31 +08:00
[CORL-174] premod user (#2572)
* add types for setting user premod * add types, fields, and mutations for user premod * enforce premoderation on users marked premod * add user premod to dropdown * update specs * fix tests * update premod workflow to match v4 * allow user filtering by premod status * add premod user history to account actions drawer * update snaps * fix permissions on new premod fields * update strings * fix tests * clean up formatting and copy * fix: nullable fixes pre-migration
This commit is contained in:
committed by
Wyatt Johnson
parent
a5c3e94751
commit
e61e62a238
@@ -1,14 +1,19 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import BanAction, { BanActionProps } from "./BanAction";
|
||||
import PremodAction, { PremodActionProps } from "./PremodAction";
|
||||
import SuspensionAction, { SuspensionActionProps } from "./SuspensionAction";
|
||||
import UsernameChangeAction, {
|
||||
UsernameChangeActionProps,
|
||||
} from "./UsernameChangeAction";
|
||||
|
||||
export interface HistoryActionProps {
|
||||
kind: "username" | "suspension" | "ban";
|
||||
action: UsernameChangeActionProps | SuspensionActionProps | BanActionProps;
|
||||
kind: "username" | "suspension" | "ban" | "premod";
|
||||
action:
|
||||
| UsernameChangeActionProps
|
||||
| SuspensionActionProps
|
||||
| BanActionProps
|
||||
| PremodActionProps;
|
||||
}
|
||||
|
||||
const AccountHistoryAction: FunctionComponent<HistoryActionProps> = ({
|
||||
@@ -22,6 +27,8 @@ const AccountHistoryAction: FunctionComponent<HistoryActionProps> = ({
|
||||
return <SuspensionAction {...action as SuspensionActionProps} />;
|
||||
case "ban":
|
||||
return <BanAction {...action as BanActionProps} />;
|
||||
case "premod":
|
||||
return <PremodAction {...action as PremodActionProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
export interface PremodActionProps {
|
||||
action: "created" | "removed";
|
||||
}
|
||||
|
||||
const PremodAction: FunctionComponent<PremodActionProps> = ({ action }) =>
|
||||
action === "created" ? (
|
||||
<Localized id="moderate-user-drawer-account-history-premod-set">
|
||||
<span>Set always premoderate</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="moderate-user-drawer-account-history-premod-removed">
|
||||
<span>Removed always premoderate</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default PremodAction;
|
||||
@@ -107,6 +107,21 @@ const UserDrawerAccountHistory: FunctionComponent<Props> = ({ user }) => {
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (user.status.premod) {
|
||||
// Merge in all the premod history items.
|
||||
user.status.premod.history.forEach(record => {
|
||||
history.push({
|
||||
kind: "premod",
|
||||
action: {
|
||||
action: record.active ? "created" : "removed",
|
||||
},
|
||||
date: new Date(record.createdAt),
|
||||
takenBy: record.createdBy ? record.createdBy.username : system,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
user.status.username.history.forEach((record, i) => {
|
||||
history.push({
|
||||
kind: "username",
|
||||
@@ -190,6 +205,15 @@ const enhanced = withFragmentContainer<any>({
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
premod {
|
||||
history {
|
||||
active
|
||||
createdBy {
|
||||
username
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
suspension {
|
||||
history {
|
||||
active
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.card {
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
|
||||
import styles from "./PremodModal.css";
|
||||
|
||||
interface Props {
|
||||
username: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const PremodModal: FunctionComponent<Props> = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
username,
|
||||
}) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="PremodModal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized
|
||||
id="community-premodModal-areYouSure"
|
||||
strong={<strong />}
|
||||
$username={username || <NotAvailable />}
|
||||
>
|
||||
<Typography variant="header2" id="PremodModal-title">
|
||||
Are you sure you want to always premoderate{" "}
|
||||
<strong>{username || <NotAvailable />}</strong>?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="community-premodModal-consequence">
|
||||
<Typography>
|
||||
Note: Always premoderating this user will place all of their
|
||||
comments in the Pre-Moderate queue.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<Flex justifyContent="flex-end" itemGutter>
|
||||
<Localized id="community-premodModal-cancel">
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
|
||||
<Localized id="community-premodModal-premodUser">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
onClick={onConfirm}
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Yes, always premoderate user
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PremodModal;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { PremodUserMutation as MutationTypes } from "coral-admin/__generated__/PremodUserMutation.graphql";
|
||||
import { getViewer } from "coral-framework/helpers";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const PremodUserMutation = createMutation(
|
||||
"premodUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
const viewer = getViewer(environment)!;
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation PremodUserMutation($input: PremodUserInput!) {
|
||||
premodUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
premod {
|
||||
active
|
||||
history {
|
||||
active
|
||||
createdAt
|
||||
createdBy {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
premodUser: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status.current.concat(GQLUSER_STATUS.PREMOD),
|
||||
premod: {
|
||||
active: true,
|
||||
history: [
|
||||
{
|
||||
active: true,
|
||||
createdAt: new Date(),
|
||||
createdBy: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default PremodUserMutation;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { RemoveUserPremodMutation as MutationTypes } from "coral-admin/__generated__/RemoveUserPremodMutation.graphql";
|
||||
import { getViewer } from "coral-framework/helpers";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RemoveUserPremodMutation = createMutation(
|
||||
"removeUserPremod",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
const viewer = getViewer(environment)!;
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RemoveUserPremodMutation($input: RemovePremodUserInput!) {
|
||||
removeUserPremod(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
premod {
|
||||
active
|
||||
history {
|
||||
active
|
||||
createdAt
|
||||
createdBy {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeUserPremod: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status.current.filter(s => s !== GQLUSER_STATUS.PREMOD),
|
||||
premod: {
|
||||
active: false,
|
||||
history: [
|
||||
{
|
||||
active: false,
|
||||
createdAt: new Date(),
|
||||
createdBy: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default RemoveUserPremodMutation;
|
||||
@@ -7,6 +7,7 @@ import { PropTypesOf } from "coral-ui/types";
|
||||
interface Props {
|
||||
banned: boolean;
|
||||
suspended: boolean;
|
||||
premod: boolean;
|
||||
}
|
||||
|
||||
const render = (
|
||||
@@ -39,6 +40,15 @@ const UserStatus: FunctionComponent<Props> = props => {
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
if (props.premod) {
|
||||
return render(
|
||||
"warning",
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
<Localized id="userStatus-premod">
|
||||
<div>Always Premoderated</div>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return render(
|
||||
"success",
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
|
||||
@@ -18,8 +18,11 @@ interface Props {
|
||||
onRemoveBan: () => void;
|
||||
onSuspend: () => void;
|
||||
onRemoveSuspension: () => void;
|
||||
onPremod: () => void;
|
||||
onRemovePremod: () => void;
|
||||
banned: boolean;
|
||||
suspended: boolean;
|
||||
premod: boolean;
|
||||
children: React.ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
@@ -29,8 +32,11 @@ const UserStatusChange: FunctionComponent<Props> = ({
|
||||
onRemoveBan,
|
||||
onSuspend,
|
||||
onRemoveSuspension,
|
||||
onPremod,
|
||||
onRemovePremod,
|
||||
banned,
|
||||
suspended,
|
||||
premod,
|
||||
children,
|
||||
fullWidth = true,
|
||||
}) => (
|
||||
@@ -42,21 +48,8 @@ const UserStatusChange: FunctionComponent<Props> = ({
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside onClickOutside={toggleVisibility}>
|
||||
<Dropdown>
|
||||
{!banned && (
|
||||
<Localized id="community-userStatus-banUser">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
onBan();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Ban User
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{banned && (
|
||||
<Localized id="community-userStatus-removeBan">
|
||||
{banned ? (
|
||||
<Localized id="community-userStatus-removeUserBan">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
@@ -64,25 +57,24 @@ const UserStatusChange: FunctionComponent<Props> = ({
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Remove Ban
|
||||
Remove ban
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{!suspended && (
|
||||
<Localized id="community-userStatus-suspendUser">
|
||||
) : (
|
||||
<Localized id="community-userStatus-ban">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
onSuspend();
|
||||
onBan();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Suspend User
|
||||
Ban
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{suspended && (
|
||||
<Localized id="community-userStatus-removeSuspension">
|
||||
{suspended ? (
|
||||
<Localized id="community-userStatus-removeUserSuspension">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
@@ -90,7 +82,44 @@ const UserStatusChange: FunctionComponent<Props> = ({
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Remove Suspension
|
||||
Remove suspension
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="community-userStatus-suspend">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
onSuspend();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Suspend
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
{premod ? (
|
||||
<Localized id="community-userStatus-removePremod">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
onRemovePremod();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Remove always pre-moderate
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="community-userStatus-premodUser">
|
||||
<DropdownButton
|
||||
className={styles.dropdownButton}
|
||||
onClick={() => {
|
||||
onPremod();
|
||||
toggleVisibility();
|
||||
}}
|
||||
>
|
||||
Always pre-moderate
|
||||
</DropdownButton>
|
||||
</Localized>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,10 @@ import { GQLUSER_ROLE } from "coral-framework/schema";
|
||||
import ButtonPadding from "../ButtonPadding";
|
||||
import BanModal from "./BanModal";
|
||||
import BanUserMutation from "./BanUserMutation";
|
||||
import PremodModal from "./PremodModal";
|
||||
import PremodUserMutation from "./PremodUserMutation";
|
||||
import RemoveUserBanMutation from "./RemoveUserBanMutation";
|
||||
import RemoveUserPremodMudtaion from "./RemoveUserPremodMutation";
|
||||
import RemoveUserSuspensionMutation from "./RemoveUserSuspensionMutation";
|
||||
import SuspendModal from "./SuspendModal";
|
||||
import SuspendUserMutation from "./SuspendUserMutation";
|
||||
@@ -31,6 +34,9 @@ const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
const suspendUser = useMutation(SuspendUserMutation);
|
||||
const removeUserBan = useMutation(RemoveUserBanMutation);
|
||||
const removeUserSuspension = useMutation(RemoveUserSuspensionMutation);
|
||||
const premodUser = useMutation(PremodUserMutation);
|
||||
const removeUserPremod = useMutation(RemoveUserPremodMudtaion);
|
||||
const [showPremod, setShowPremod] = useState<boolean>(false);
|
||||
const [showBanned, setShowBanned] = useState<boolean>(false);
|
||||
const [showSuspend, setShowSuspend] = useState<boolean>(false);
|
||||
const [showSuspendSuccess, setShowSuspendSuccess] = useState<boolean>(false);
|
||||
@@ -59,6 +65,31 @@ const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
removeUserSuspension({ userID: user.id });
|
||||
}, [user, removeUserSuspension]);
|
||||
|
||||
const handlePremod = useCallback(() => {
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (user.status.premod && user.status.premod.active) {
|
||||
return;
|
||||
}
|
||||
setShowPremod(true);
|
||||
}, [user, setShowPremod]);
|
||||
|
||||
const handlePremodConfirm = useCallback(() => {
|
||||
premodUser({ userID: user.id });
|
||||
setShowPremod(false);
|
||||
}, [premodUser, user, setShowPremod]);
|
||||
|
||||
const hidePremod = useCallback(() => {
|
||||
setShowPremod(false);
|
||||
}, [setShowPremod]);
|
||||
|
||||
const handleRemovePremod = useCallback(() => {
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (!user.status.premod || !user.status.premod.active) {
|
||||
return;
|
||||
}
|
||||
removeUserPremod({ userID: user.id });
|
||||
}, [user, premodUser]);
|
||||
|
||||
const handleSuspendModalClose = useCallback(() => {
|
||||
setShowSuspend(false);
|
||||
setShowSuspendSuccess(false);
|
||||
@@ -103,8 +134,12 @@ const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
onRemoveBan={handleRemoveBan}
|
||||
onSuspend={handleSuspend}
|
||||
onRemoveSuspension={handleRemoveSuspension}
|
||||
onPremod={handlePremod}
|
||||
onRemovePremod={handleRemovePremod}
|
||||
banned={user.status.ban.active}
|
||||
suspended={user.status.suspension.active}
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
premod={Boolean(user.status.premod && user.status.premod.active)}
|
||||
fullWidth={fullWidth}
|
||||
>
|
||||
<UserStatusContainer user={user} />
|
||||
@@ -117,6 +152,12 @@ const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
organizationName={settings.organization.name}
|
||||
onConfirm={handleSuspendConfirm}
|
||||
/>
|
||||
<PremodModal
|
||||
username={user.username}
|
||||
open={showPremod}
|
||||
onClose={hidePremod}
|
||||
onConfirm={handlePremodConfirm}
|
||||
/>
|
||||
<BanModal
|
||||
username={user.username}
|
||||
open={showBanned}
|
||||
@@ -140,6 +181,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
suspension {
|
||||
active
|
||||
}
|
||||
premod {
|
||||
active
|
||||
}
|
||||
}
|
||||
...UserStatusContainer_user
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const UserStatusContainer: FunctionComponent<Props> = props => {
|
||||
<UserStatus
|
||||
banned={props.user.status.current.includes(GQLUSER_STATUS.BANNED)}
|
||||
suspended={props.user.status.current.includes(GQLUSER_STATUS.SUSPENDED)}
|
||||
premod={props.user.status.current.includes(GQLUSER_STATUS.PREMOD)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -178,6 +178,11 @@ const UserTableFilter: FunctionComponent<Props> = props => (
|
||||
<Localized id="userStatus-banned">
|
||||
<Option value={GQLUSER_STATUS.BANNED}>Banned</Option>
|
||||
</Localized>
|
||||
<Localized id="userStatus-premod">
|
||||
<Option value={GQLUSER_STATUS.PREMOD}>
|
||||
Always Premoderate
|
||||
</Option>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</Localized>
|
||||
</Flex>
|
||||
|
||||
@@ -166,6 +166,11 @@ exports[`renders community 1`] = `
|
||||
>
|
||||
Banned
|
||||
</option>
|
||||
<option
|
||||
value="PREMOD"
|
||||
>
|
||||
Always pre-moderate
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
@@ -825,6 +830,11 @@ exports[`renders empty community 1`] = `
|
||||
>
|
||||
Banned
|
||||
</option>
|
||||
<option
|
||||
value="PREMOD"
|
||||
>
|
||||
Always pre-moderate
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
|
||||
@@ -446,7 +446,7 @@ it("suspend user", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.getByText("Suspend", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
@@ -515,7 +515,7 @@ it("remove user suspension", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Remove Suspension", { selector: "button" })
|
||||
.getByText("Remove suspension", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
expect(resolvers.Mutation!.removeUserSuspension!.called).toBe(true);
|
||||
@@ -566,7 +566,7 @@ it("suspend user with custom timeout", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.getByText("Suspend", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
@@ -632,7 +632,7 @@ it("suspend user with custom message", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.getByText("Suspend", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
@@ -704,7 +704,7 @@ it("ban user", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Ban User", { selector: "button" })
|
||||
.getByText("Ban", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
@@ -767,7 +767,7 @@ it("ban user with custom message", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Ban User", { selector: "button" })
|
||||
.getByText("Ban", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
@@ -853,7 +853,7 @@ it("remove user ban", async () => {
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Remove Ban", { selector: "button" })
|
||||
.getByText("Remove ban", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
|
||||
@@ -321,6 +321,10 @@ export const baseUser = createFixture<GQLUser>({
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
premod: {
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ export function createUserStatus(banned: boolean = false): GQLUserStatus {
|
||||
username: {
|
||||
history: [],
|
||||
},
|
||||
premod: {
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ export function createUserStatus(banned: boolean = false) {
|
||||
until: null,
|
||||
history: [],
|
||||
},
|
||||
premod: {
|
||||
active: false,
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,6 +147,7 @@ export function createComment(author?: GQLUser) {
|
||||
COMMENT_DETECTED_LINKS: 0,
|
||||
COMMENT_DETECTED_BANNED_WORD: 0,
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_DETECTED_PREMOD_USER: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -314,4 +314,6 @@ export enum ERROR_CODES {
|
||||
* all non-admin users.
|
||||
*/
|
||||
RAW_QUERY_NOT_AUTHORIZED = "RAW_QUERY_NOT_AUTHORIZED",
|
||||
|
||||
USER_ALREADY_PREMOD = "USER_ALREADY_PREMOD",
|
||||
}
|
||||
|
||||
@@ -593,6 +593,14 @@ export class UserAlreadySuspendedError extends CoralError {
|
||||
}
|
||||
}
|
||||
|
||||
export class UserAlreadyPremoderated extends CoralError {
|
||||
constructor() {
|
||||
super({
|
||||
code: ERROR_CODES.USER_ALREADY_PREMOD,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class UserAlreadyBannedError extends CoralError {
|
||||
constructor() {
|
||||
super({
|
||||
|
||||
@@ -54,4 +54,5 @@ export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
|
||||
USERNAME_UPDATED_WITHIN_WINDOW: "error-usernameAlreadyUpdated",
|
||||
PERSISTED_QUERY_NOT_FOUND: "error-persistedQueryNotFound",
|
||||
RAW_QUERY_NOT_AUTHORIZED: "error-rawQueryNotAuthorized",
|
||||
USER_ALREADY_PREMOD: "error-userAlreadyPremod",
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ const statusFilter = (
|
||||
case GQLUSER_STATUS.ACTIVE:
|
||||
return {
|
||||
"status.ban.active": false,
|
||||
"status.premod.active": false,
|
||||
"status.suspension.history": {
|
||||
$not: {
|
||||
$elemMatch: {
|
||||
@@ -55,6 +56,8 @@ const statusFilter = (
|
||||
};
|
||||
case GQLUSER_STATUS.BANNED:
|
||||
return { "status.ban.active": true };
|
||||
case GQLUSER_STATUS.PREMOD:
|
||||
return { "status.premod.active": true };
|
||||
case GQLUSER_STATUS.SUSPENDED:
|
||||
return {
|
||||
"status.suspension.history": {
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
createToken,
|
||||
deactivateToken,
|
||||
ignore,
|
||||
premod,
|
||||
removeBan,
|
||||
removeIgnore,
|
||||
removePremod,
|
||||
removeSuspension,
|
||||
requestAccountDeletion,
|
||||
requestCommentsDownload,
|
||||
@@ -38,6 +40,8 @@ import {
|
||||
GQLDeleteUserAccountInput,
|
||||
GQLIgnoreUserInput,
|
||||
GQLInviteUsersInput,
|
||||
GQLPremodUserInput,
|
||||
GQLRemovePremodUserInput,
|
||||
GQLRemoveUserBanInput,
|
||||
GQLRemoveUserIgnoreInput,
|
||||
GQLRemoveUserSuspensionInput,
|
||||
@@ -209,6 +213,8 @@ export const Users = (ctx: TenantContext) => ({
|
||||
input.message,
|
||||
ctx.now
|
||||
),
|
||||
premodUser: async (input: GQLPremodUserInput) =>
|
||||
premod(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
|
||||
suspend: async (input: GQLSuspendUserInput) =>
|
||||
suspend(
|
||||
ctx.mongo,
|
||||
@@ -224,6 +230,8 @@ export const Users = (ctx: TenantContext) => ({
|
||||
removeBan(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
|
||||
removeSuspension: async (input: GQLRemoveUserSuspensionInput) =>
|
||||
removeSuspension(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
|
||||
removeUserPremod: async (input: GQLRemovePremodUserInput) =>
|
||||
removePremod(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
|
||||
ignore: async (input: GQLIgnoreUserInput) =>
|
||||
ignore(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
|
||||
removeIgnore: async (input: GQLRemoveUserIgnoreInput) =>
|
||||
|
||||
@@ -181,6 +181,14 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
user: await ctx.mutators.Users.suspend(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
premodUser: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.Users.premodUser(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
removeUserPremod: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.Users.removeUserPremod(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
removeUserSuspension: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.Users.removeSuspension(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GQLPremodStatusTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import * as user from "coral-server/models/user";
|
||||
|
||||
export type PremodStatusInput = user.ConsolidatedPremodStatus & {
|
||||
userID: string;
|
||||
};
|
||||
|
||||
export const PremodStatus: Required<
|
||||
GQLPremodStatusTypeResolver<PremodStatusInput>
|
||||
> = {
|
||||
active: ({ active }) => active,
|
||||
history: ({ history, userID }) =>
|
||||
history.map(status => ({ ...status, userID })),
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { GQLPremodStatusHistoryTypeResolver } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import * as user from "coral-server/models/user";
|
||||
|
||||
export const PremodStatusHistory: Required<
|
||||
GQLPremodStatusHistoryTypeResolver<user.PremodStatusHistory>
|
||||
> = {
|
||||
active: ({ active }) => active,
|
||||
createdBy: ({ createdBy }, input, ctx) => {
|
||||
if (createdBy) {
|
||||
return ctx.loaders.Users.user.load(createdBy);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
createdAt: ({ createdAt }) => createdAt,
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import * as user from "coral-server/models/user";
|
||||
|
||||
import { BanStatusInput } from "./BanStatus";
|
||||
import { PremodStatusInput } from "./PremodStatus";
|
||||
import { SuspensionStatusInput } from "./SuspensionStatus";
|
||||
import { UsernameStatusInput } from "./UsernameStatus";
|
||||
|
||||
@@ -29,6 +30,12 @@ export const UserStatus: Required<
|
||||
statuses.push(GQLUSER_STATUS.SUSPENDED);
|
||||
}
|
||||
|
||||
// If they are set to mandatory premod, then mark it.
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (consolidatedStatus.premod && consolidatedStatus.premod.active) {
|
||||
statuses.push(GQLUSER_STATUS.PREMOD);
|
||||
}
|
||||
|
||||
// If no other statuses were applied, then apply the active status.
|
||||
if (statuses.length === 0) {
|
||||
statuses.push(GQLUSER_STATUS.ACTIVE);
|
||||
@@ -48,4 +55,17 @@ export const UserStatus: Required<
|
||||
...user.consolidateUserSuspensionStatus(suspension),
|
||||
userID,
|
||||
}),
|
||||
// FIXME: (wyattjoh) once migration has been performed, return PremodStatusInput only
|
||||
premod: ({ premod, userID }): PremodStatusInput | null => {
|
||||
const status = user.consolidateUserPremodStatus(premod);
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...status,
|
||||
userID,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,6 +29,8 @@ import { ModerationQueue } from "./ModerationQueue";
|
||||
import { ModerationQueues } from "./ModerationQueues";
|
||||
import { Mutation } from "./Mutation";
|
||||
import { OIDCAuthIntegration } from "./OIDCAuthIntegration";
|
||||
import { PremodStatus } from "./PremodStatus";
|
||||
import { PremodStatusHistory } from "./PremodStatusHistory";
|
||||
import { Profile } from "./Profile";
|
||||
import { Query } from "./Query";
|
||||
import { RecentCommentHistory } from "./RecentCommentHistory";
|
||||
@@ -72,6 +74,8 @@ const Resolvers: GQLResolver = {
|
||||
ModerationQueues,
|
||||
Mutation,
|
||||
OIDCAuthIntegration,
|
||||
PremodStatus,
|
||||
PremodStatusHistory,
|
||||
Profile,
|
||||
Query,
|
||||
RecentCommentHistory,
|
||||
|
||||
@@ -141,6 +141,11 @@ enum COMMENT_FLAG_DETECTED_REASON {
|
||||
recent history of rejected comments.
|
||||
"""
|
||||
COMMENT_DETECTED_RECENT_HISTORY
|
||||
|
||||
"""
|
||||
COMMENT_DETECTED_PREMOD_USER is used when a Comment author has been tagged as requiring premoderation
|
||||
"""
|
||||
COMMENT_DETECTED_PREMOD_USER
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -157,6 +162,7 @@ enum COMMENT_FLAG_REASON {
|
||||
COMMENT_DETECTED_BANNED_WORD
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
COMMENT_DETECTED_RECENT_HISTORY
|
||||
COMMENT_DETECTED_PREMOD_USER
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -191,6 +197,7 @@ type FlagReasonActionCounts {
|
||||
COMMENT_DETECTED_BANNED_WORD: Int!
|
||||
COMMENT_DETECTED_SUSPECT_WORD: Int!
|
||||
COMMENT_DETECTED_RECENT_HISTORY: Int!
|
||||
COMMENT_DETECTED_PREMOD_USER: Int!
|
||||
}
|
||||
|
||||
type Flag {
|
||||
@@ -1440,6 +1447,34 @@ type SuspensionStatus {
|
||||
history: [SuspensionStatusHistory!]! @auth(roles: [ADMIN, MODERATOR])
|
||||
}
|
||||
|
||||
type PremodStatusHistory {
|
||||
"""
|
||||
active when true, indicates that the given user is premodded.
|
||||
"""
|
||||
active: Boolean!
|
||||
"""
|
||||
createdBy is the user that flagged the commenter as pre-mod
|
||||
"""
|
||||
createdBy: User!
|
||||
|
||||
"""
|
||||
createdAt is the time the user was set to pre-mod
|
||||
"""
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
type PremodStatus {
|
||||
"""
|
||||
active when true, indicates that the given user is set to pre-mod.
|
||||
"""
|
||||
active: Boolean!
|
||||
|
||||
"""
|
||||
history is the list of all suspension events against a specific User.
|
||||
"""
|
||||
history: [PremodStatusHistory!]!
|
||||
}
|
||||
|
||||
type UsernameHistory {
|
||||
"""
|
||||
username is the username that was assigned
|
||||
@@ -1503,6 +1538,13 @@ type UserStatus {
|
||||
changes.
|
||||
"""
|
||||
suspension: SuspensionStatus!
|
||||
|
||||
"""
|
||||
premod stores the user premod status as well as the history of changes.
|
||||
|
||||
FIXME: (wyattjoh) once migration has been performed, make non-nullable
|
||||
"""
|
||||
premod: PremodStatus @auth(roles: [ADMIN, MODERATOR])
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1524,6 +1566,11 @@ enum USER_STATUS {
|
||||
SUSPENDED is used when a User is currently suspended.
|
||||
"""
|
||||
SUSPENDED
|
||||
|
||||
"""
|
||||
PREMOD is used when a User is currently set to require pre-moderation.
|
||||
"""
|
||||
PREMOD
|
||||
}
|
||||
|
||||
enum DIGEST_FREQUENCY {
|
||||
@@ -4771,6 +4818,62 @@ type RemoveUserSuspensionPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# premodUser
|
||||
##################
|
||||
|
||||
input PremodUserInput {
|
||||
"""
|
||||
userID is the ID of the User that should be premodded.
|
||||
"""
|
||||
userID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type PremodUserPayload {
|
||||
"""
|
||||
user is the possibly modified User.
|
||||
"""
|
||||
user: User!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# removePremod
|
||||
##################
|
||||
|
||||
input RemovePremodUserInput {
|
||||
"""
|
||||
userID is the ID of the User that should be premodded.
|
||||
"""
|
||||
userID: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type RemovePremodUserPayload {
|
||||
"""
|
||||
user is the possibly modified User.
|
||||
"""
|
||||
user: User!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# ignoreUser
|
||||
##################
|
||||
@@ -5196,6 +5299,18 @@ type Mutation {
|
||||
requestUserCommentsDownload(
|
||||
input: RequestUserCommentsDownloadInput!
|
||||
): RequestUserCommentsDownloadPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
premodUser sets a user to mandatory premod
|
||||
"""
|
||||
premodUser(input: PremodUserInput!): PremodUserPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
removeUserPremod removes a user from mandatory premod
|
||||
"""
|
||||
removeUserPremod(input: RemovePremodUserInput!): RemovePremodUserPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
}
|
||||
|
||||
##################
|
||||
@@ -5367,7 +5482,7 @@ type Subscription {
|
||||
commentReleased returns when a Comment on a premoderated stream is approved
|
||||
"""
|
||||
commentReleased(storyID: ID!): CommentReleasedPayload!
|
||||
|
||||
|
||||
"""
|
||||
commentReplyCreated returns when a Comment is posted in the ancestor chain of
|
||||
comments.
|
||||
|
||||
@@ -19,6 +19,7 @@ Object {
|
||||
"reasons": Object {
|
||||
"COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"COMMENT_DETECTED_LINKS": 0,
|
||||
"COMMENT_DETECTED_PREMOD_USER": 0,
|
||||
"COMMENT_DETECTED_RECENT_HISTORY": 0,
|
||||
"COMMENT_DETECTED_SPAM": 0,
|
||||
"COMMENT_DETECTED_SUSPECT_WORD": 0,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PasswordResetTokenExpired,
|
||||
TokenNotFoundError,
|
||||
UserAlreadyBannedError,
|
||||
UserAlreadyPremoderated,
|
||||
UserAlreadySuspendedError,
|
||||
UsernameAlreadySetError,
|
||||
UserNotFoundError,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import {
|
||||
GQLBanStatus,
|
||||
GQLDIGEST_FREQUENCY,
|
||||
GQLPremodStatus,
|
||||
GQLSuspensionStatus,
|
||||
GQLTimeRange,
|
||||
GQLUSER_ROLE,
|
||||
@@ -236,6 +238,42 @@ export interface UsernameStatus {
|
||||
history: UsernameHistory[];
|
||||
}
|
||||
|
||||
/**
|
||||
* PremodStatusHistory is the history of premod status changes
|
||||
* against a specific User.
|
||||
*/
|
||||
export interface PremodStatusHistory {
|
||||
/**
|
||||
* active when true, indicates that the given user is premodded.
|
||||
*/
|
||||
active: boolean;
|
||||
/**
|
||||
* createdBy is the ID for the User that premodded the User. If `null`, the
|
||||
* premod was created by the system.
|
||||
*/
|
||||
createdBy?: string;
|
||||
|
||||
/**
|
||||
* createdAt is the time that the given premod status was set.
|
||||
*/
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* PremodStatus is the status of whether a user is set to mandatory premod
|
||||
*/
|
||||
export interface PremodStatus {
|
||||
/**
|
||||
* active when true, indicates that the given user is set to mandatory premod.
|
||||
*/
|
||||
active: boolean;
|
||||
|
||||
/**
|
||||
* history is a list of previous enable/disable of premod status
|
||||
*/
|
||||
history: PremodStatusHistory[];
|
||||
}
|
||||
|
||||
/**
|
||||
* UserStatus stores the user status information regarding moderation state.
|
||||
*/
|
||||
@@ -255,6 +293,14 @@ export interface UserStatus {
|
||||
* username stores the history of username changes for this user.
|
||||
*/
|
||||
username: UsernameStatus;
|
||||
|
||||
/**
|
||||
* premod stores whether a user is set to mandatory premod and history of
|
||||
* premod status.
|
||||
*
|
||||
* FIXME: (wyattjoh) set defaults during migration
|
||||
*/
|
||||
premod?: PremodStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,6 +549,7 @@ async function findOrCreateUserInput(
|
||||
},
|
||||
suspension: { history: [] },
|
||||
ban: { active: false, history: [] },
|
||||
premod: { active: false, history: [] },
|
||||
},
|
||||
notifications: {
|
||||
onReply: false,
|
||||
@@ -1358,6 +1405,130 @@ async function retrieveConnection(
|
||||
return resolveConnection(query, input, user => user.createdAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* premodUser will set a user to mandatory premod.
|
||||
*
|
||||
* @param mongo the mongo database handle
|
||||
* @param tenantID the Tenant's ID where the User exists
|
||||
* @param id the ID of the user being banned
|
||||
* @param createdBy the ID of the user premodding
|
||||
* @param now the current date
|
||||
*/
|
||||
export async function premodUser(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
createdBy: string,
|
||||
now = new Date()
|
||||
) {
|
||||
// Create the new ban.
|
||||
const premodStatusHistory: PremodStatusHistory = {
|
||||
active: true,
|
||||
createdBy,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
// Try to update the user if the user isn't already banned.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
id,
|
||||
tenantID,
|
||||
"status.premod.active": {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"status.premod.active": true,
|
||||
},
|
||||
$push: {
|
||||
"status.premod.history": premodStatusHistory,
|
||||
},
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
if (!result.value) {
|
||||
// Get the user so we can figure out why the ban operation failed.
|
||||
const user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
|
||||
// Check to see if the user is already banned.
|
||||
const premod = consolidateUserPremodStatus(user.status.premod);
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (premod && premod.active) {
|
||||
throw new UserAlreadyPremoderated();
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occurred");
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* removeUserPremod will lift a user premod requirement
|
||||
* @param mongo the mongo database handle
|
||||
* @param tenantID the Tenant's ID where the User exists
|
||||
* @param id the ID of the user having their ban lifted
|
||||
* @param modifiedBy the ID of the user lifting the premod
|
||||
* @param now the current date
|
||||
*/
|
||||
export async function removeUserPremod(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
createdBy: string,
|
||||
now = new Date()
|
||||
) {
|
||||
// Create the new ban.
|
||||
const premod: PremodStatusHistory = {
|
||||
active: false,
|
||||
createdBy,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
// Try to update the user if the user isn't already banned.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
id,
|
||||
tenantID,
|
||||
"status.premod.active": true,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"status.premod.active": false,
|
||||
},
|
||||
$push: {
|
||||
"status.premod.history": premod,
|
||||
},
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.value) {
|
||||
// Get the user so we can figure out why the ban operation failed.
|
||||
const user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
|
||||
// The user wasn't banned already, so nothing needs to be done!
|
||||
return user;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* banUser will ban a specific user from interacting with the site.
|
||||
*
|
||||
@@ -1651,18 +1822,23 @@ export type ConsolidatedBanStatus = Omit<GQLBanStatus, "history"> &
|
||||
export type ConsolidatedUsernameStatus = Omit<GQLUsernameStatus, "history"> &
|
||||
Pick<UsernameStatus, "history">;
|
||||
|
||||
export type ConsolidatedPremodStatus = Omit<GQLPremodStatus, "history"> &
|
||||
Pick<PremodStatus, "history">;
|
||||
|
||||
export function consolidateUsernameStatus(
|
||||
username: User["status"]["username"]
|
||||
): ConsolidatedUsernameStatus {
|
||||
) {
|
||||
return username;
|
||||
}
|
||||
|
||||
export function consolidateUserBanStatus(
|
||||
ban: User["status"]["ban"]
|
||||
): ConsolidatedBanStatus {
|
||||
export function consolidateUserBanStatus(ban: User["status"]["ban"]) {
|
||||
return ban;
|
||||
}
|
||||
|
||||
export function consolidateUserPremodStatus(premod: User["status"]["premod"]) {
|
||||
return premod;
|
||||
}
|
||||
|
||||
export type ConsolidatedSuspensionStatus = Omit<
|
||||
GQLSuspensionStatus,
|
||||
"history"
|
||||
@@ -1697,6 +1873,8 @@ export function consolidateUserSuspensionStatus(
|
||||
export interface ConsolidatedUserStatus {
|
||||
suspension: ConsolidatedSuspensionStatus;
|
||||
ban: ConsolidatedBanStatus;
|
||||
// FIXME: (wyattjoh) once migration has been performed, make required
|
||||
premod?: ConsolidatedPremodStatus;
|
||||
}
|
||||
|
||||
export function consolidateUserStatus(
|
||||
@@ -1707,6 +1885,7 @@ export function consolidateUserStatus(
|
||||
return {
|
||||
suspension: consolidateUserSuspensionStatus(status.suspension, now),
|
||||
ban: consolidateUserBanStatus(status.ban),
|
||||
premod: consolidateUserPremodStatus(status.premod),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { commentLength } from "./commentLength";
|
||||
import { detectLinks } from "./detectLinks";
|
||||
import { linkify } from "./linkify";
|
||||
import { preModerate } from "./preModerate";
|
||||
import { premodUser } from "./preModerateUser";
|
||||
import { purify } from "./purify";
|
||||
import { recentCommentHistory } from "./recentCommentHistory";
|
||||
import { spam } from "./spam";
|
||||
@@ -29,4 +30,5 @@ export const moderationPhases: IntermediateModerationPhase[] = [
|
||||
spam,
|
||||
detectLinks,
|
||||
preModerate,
|
||||
premodUser,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { GQLCOMMENT_STATUS } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
} from "coral-server/services/comments/pipeline";
|
||||
|
||||
// If a given user is set to always premod, set to premod.
|
||||
export const premodUser: IntermediateModerationPhase = ({
|
||||
author,
|
||||
}): IntermediatePhaseResult | void => {
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (author.status.premod && author.status.premod.active) {
|
||||
return {
|
||||
status: GQLCOMMENT_STATUS.PREMOD,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PasswordIncorrect,
|
||||
TokenNotFoundError,
|
||||
UserAlreadyBannedError,
|
||||
UserAlreadyPremoderated,
|
||||
UserAlreadySuspendedError,
|
||||
UserCannotBeIgnoredError,
|
||||
UsernameAlreadySetError,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
banUser,
|
||||
clearDeletionDate,
|
||||
consolidateUserBanStatus,
|
||||
consolidateUserPremodStatus,
|
||||
consolidateUserSuspensionStatus,
|
||||
createUser,
|
||||
createUserToken,
|
||||
@@ -41,9 +43,11 @@ import {
|
||||
FindOrCreateUserInput,
|
||||
ignoreUser,
|
||||
NotificationSettingsInput,
|
||||
premodUser,
|
||||
removeActiveUserSuspensions,
|
||||
removeUserBan,
|
||||
removeUserIgnore,
|
||||
removeUserPremod,
|
||||
retrieveUser,
|
||||
retrieveUserWithEmail,
|
||||
scheduleDeletionDate,
|
||||
@@ -791,6 +795,66 @@ export async function ban(
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* premod will premod a specific user.
|
||||
*
|
||||
* @param mongo mongo database to interact with
|
||||
* @param tenant Tenant where the User will be banned on
|
||||
* @param moderator the User that is banning the User
|
||||
* @param userID the ID of the User being banned
|
||||
* @param now the current time that the ban took effect
|
||||
*/
|
||||
export async function premod(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
moderator: User,
|
||||
userID: string,
|
||||
now = new Date()
|
||||
) {
|
||||
// Get the user being banned to check to see if the user already has an
|
||||
// existing ban.
|
||||
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
||||
if (!targetUser) {
|
||||
throw new UserNotFoundError(userID);
|
||||
}
|
||||
|
||||
// Check to see if the User is currently banned.
|
||||
const premodStatus = consolidateUserPremodStatus(targetUser.status.premod);
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (premodStatus && premodStatus.active) {
|
||||
throw new UserAlreadyPremoderated();
|
||||
}
|
||||
|
||||
// Ban the user.
|
||||
return premodUser(mongo, tenant.id, userID, moderator.id, now);
|
||||
}
|
||||
|
||||
export async function removePremod(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
moderator: User,
|
||||
userID: string,
|
||||
now = new Date()
|
||||
) {
|
||||
// Get the user being suspended to check to see if the user already has an
|
||||
// existing suspension.
|
||||
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
||||
if (!targetUser) {
|
||||
throw new UserNotFoundError(userID);
|
||||
}
|
||||
|
||||
// Check to see if the User is currently suspended.
|
||||
const premodStatus = consolidateUserPremodStatus(targetUser.status.premod);
|
||||
// FIXME: (wyattjoh) once migration has been performed, remove check
|
||||
if (!premodStatus || !premodStatus.active) {
|
||||
// The user is not premodded currently, just return the user because we
|
||||
// don't have to do anything.
|
||||
return targetUser;
|
||||
}
|
||||
|
||||
// For each of the suspensions, remove it.
|
||||
return removeUserPremod(mongo, tenant.id, userID, moderator.id, now);
|
||||
}
|
||||
/**
|
||||
* suspend will suspend a give user from interacting with Coral.
|
||||
*
|
||||
|
||||
@@ -22,6 +22,7 @@ role-plural-commenter = Commenters
|
||||
userStatus-active = Active
|
||||
userStatus-banned = Banned
|
||||
userStatus-suspended = Suspended
|
||||
userStatus-premod = Always pre-moderate
|
||||
|
||||
## Navigation
|
||||
navigation-moderate = Moderate
|
||||
@@ -486,6 +487,9 @@ moderate-user-drawer-username-change = Username change
|
||||
moderate-user-drawer-username-change-new = New:
|
||||
moderate-user-drawer-username-change-old = Old:
|
||||
|
||||
moderate-user-drawer-account-history-premod-set = Always pre-moderate
|
||||
moderate-user-drawer-account-history-premod-removed = Removed pre-moderate
|
||||
|
||||
moderate-user-drawer-suspension =
|
||||
Suspension, { $value } { $unit ->
|
||||
[second] { $value ->
|
||||
@@ -614,12 +618,18 @@ community-userStatus-popover =
|
||||
.description = A dropdown to change the user status
|
||||
|
||||
community-userStatus-banUser = Ban User
|
||||
community-userStatus-ban = Ban
|
||||
community-userStatus-removeBan = Remove Ban
|
||||
community-userStatus-removeUserBan = Remove ban
|
||||
community-userStatus-suspendUser = Suspend User
|
||||
community-userStatus-suspend = Suspend
|
||||
community-userStatus-removeSuspension = Remove Suspension
|
||||
community-userStatus-removeUserSuspension = Remove suspension
|
||||
community-userStatus-unknown = Unknown
|
||||
community-userStatus-changeButton =
|
||||
.aria-label = Change user status
|
||||
community-userStatus-premodUser = Always pre-moderate
|
||||
community-userStatus-removePremod = Remove pre-moderate
|
||||
|
||||
community-banModal-areYouSure = Are you sure you want to ban <strong>{ $username }</strong>?
|
||||
community-banModal-consequence =
|
||||
@@ -652,6 +662,13 @@ community-suspendModal-success =
|
||||
community-suspendModal-success-close = Close
|
||||
community-suspendModal-selectDuration = Select suspension length
|
||||
|
||||
community-premodModal-areYouSure =
|
||||
Are you sure you want to always pre-moderate <strong>{ $username }</strong>?
|
||||
community-premodModal-consequence =
|
||||
All their comments will go to the Pending queue until you remove this status.
|
||||
community-premodModal-cancel = Cancel
|
||||
community-premodModal-premodUser = Yes, always pre-moderate
|
||||
|
||||
community-invite-inviteMember = Invite members to your organization
|
||||
community-invite-emailAddressLabel = Email address:
|
||||
community-invite-inviteMore = Invite more
|
||||
|
||||
Reference in New Issue
Block a user