diff --git a/bin/cli b/bin/cli
index 6237d4740..b61c529c5 100755
--- a/bin/cli
+++ b/bin/cli
@@ -41,20 +41,3 @@ if (!commands.includes(command)) {
}
process.exit(1);
}
-
-// /**
-// * When this process exists, check to see if we have a running command, if we do
-// * check to see if it is still running. If it is, then kill it with a SIGINT
-// * signal. This is for the use case where we want to kill the process that is
-// * labeled with the PID written out by the parent process.
-// */
-// process.once('exit', () => {
-// if (
-
-// // program.runningCommand &&
-// program.runningCommand.killed === false &&
-// program.runningCommand.exitCode === null
-// ) {
-// program.runningCommand.kill('SIGINT');
-// }
-// });
diff --git a/bin/cli-plugins b/bin/cli-plugins
index a99e97649..883682ede 100755
--- a/bin/cli-plugins
+++ b/bin/cli-plugins
@@ -235,7 +235,7 @@ async function reconcileLocalPlugins({ skipRemote, dryRun }) {
if (output.status) {
throw new Error(
- 'Could not install local plugin dependencies, errors occured during install'
+ 'Could not install local plugin dependencies, errors occurred during install'
);
}
@@ -253,59 +253,61 @@ async function reconcilePluginDeps({
dryRun,
upgradeRemote,
}) {
- let startTime = new Date();
+ try {
+ let startTime = new Date();
- // We don't need to do anything if we skip everything....
- if (skipLocal && skipRemote) {
- return;
- }
+ // We don't need to do anything if we skip everything....
+ if (skipLocal && skipRemote) {
+ return;
+ }
- // Traverse local plugins and install dependencies if enabled.
- if (!skipLocal) {
- await reconcileLocalPlugins({ skipRemote, dryRun });
- }
+ // Traverse local plugins and install dependencies if enabled.
+ if (!skipLocal) {
+ await reconcileLocalPlugins({ skipRemote, dryRun });
+ }
- // Locate any external plugins and install them.
- if (!skipRemote) {
- let results = [];
- try {
- results = await reconcileRemotePlugins({
+ // Locate any external plugins and install them.
+ if (!skipRemote) {
+ const results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
- } catch (e) {
- throw e;
+
+ let status;
+ if (dryRun) {
+ status = '[dry-run] success'.green;
+ } else {
+ status = 'success'.green;
+ }
+
+ let message;
+ if (results.upgradable.length === 0 && results.fetchable.length === 0) {
+ message = 'Already up-to-date.';
+ } else if (results.upgradable.length === 0) {
+ message = `Fetched ${results.fetchable.length} new plugins.`;
+ } else if (results.fetchable.length === 0) {
+ message = `Upgraded ${results.upgradable.length} new plugins.`;
+ } else {
+ message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
+ results.upgradable.length
+ } plugins.`;
+ }
+
+ console.log(`\n${status} ${message}`);
}
- let status;
- if (dryRun) {
- status = '[dry-run] success'.green;
- } else {
- status = 'success'.green;
- }
+ let endTime = new Date();
- let message;
- if (results.upgradable.length === 0 && results.fetchable.length === 0) {
- message = 'Already up-to-date.';
- } else if (results.upgradable.length === 0) {
- message = `Fetched ${results.fetchable.length} new plugins.`;
- } else if (results.fetchable.length === 0) {
- message = `Upgraded ${results.upgradable.length} new plugins.`;
- } else {
- message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
- results.upgradable.length
- } plugins.`;
- }
-
- console.log(`\n${status} ${message}`);
+ let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
+ 2
+ );
+ console.log(`✨ Done in ${totalTime}s.`);
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
}
-
- let endTime = new Date();
-
- let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
- console.log(`✨ Done in ${totalTime}s.`);
}
async function createSeedPlugin() {
diff --git a/bin/util.js b/bin/util.js
index 0a9adb966..e80f123a5 100644
--- a/bin/util.js
+++ b/bin/util.js
@@ -63,5 +63,6 @@ process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
- throw err;
+ console.error(err);
+ process.exit(1);
});
diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js
index d226ac744..71d2ed1a3 100644
--- a/client/coral-admin/src/components/UserDetail.js
+++ b/client/coral-admin/src/components/UserDetail.js
@@ -2,7 +2,6 @@ import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import capitalize from 'lodash/capitalize';
-import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import AccountHistory from './AccountHistory';
import { Slot } from 'coral-framework/components';
@@ -29,43 +28,23 @@ import UserInfoTooltip from './UserInfoTooltip';
class UserDetail extends React.Component {
rejectThenReload = async info => {
- try {
- await this.props.rejectComment(info);
- this.props.data.refetch();
- } catch (err) {
- console.error(err);
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.rejectComment(info);
+ this.props.data.refetch();
};
acceptThenReload = async info => {
- try {
- await this.props.acceptComment(info);
- this.props.data.refetch();
- } catch (err) {
- console.error(err);
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.acceptComment(info);
+ this.props.data.refetch();
};
bulkAcceptThenReload = async () => {
- try {
- await this.props.bulkAccept();
- this.props.data.refetch();
- } catch (err) {
- console.error(err);
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.bulkAccept();
+ this.props.data.refetch();
};
bulkRejectThenReload = async () => {
- try {
- await this.props.bulkReject();
- this.props.data.refetch();
- } catch (err) {
- console.error(err);
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.bulkReject();
+ this.props.data.refetch();
};
changeTab = tab => {
@@ -94,6 +73,16 @@ class UserDetail extends React.Component {
);
}
+ renderError() {
+ return (
+
+
+ {this.props.data.error.message}
+
+
+ );
+ }
+
getActionMenuLabel() {
const { root: { user } } = this.props;
@@ -345,6 +334,10 @@ class UserDetail extends React.Component {
}
render() {
+ if (this.props.data.error) {
+ return this.renderError();
+ }
+
if (this.props.loading) {
return this.renderLoading();
}
@@ -371,7 +364,6 @@ UserDetail.propTypes = {
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
- notify: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
diff --git a/client/coral-admin/src/containers/BanUserDialog.js b/client/coral-admin/src/containers/BanUserDialog.js
index d2dc642a7..3617902ff 100644
--- a/client/coral-admin/src/containers/BanUserDialog.js
+++ b/client/coral-admin/src/containers/BanUserDialog.js
@@ -10,8 +10,6 @@ import {
} from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
-import { getErrorMessages } from 'coral-framework/utils';
-import { notify } from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
@@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component {
banUser,
setCommentStatus,
hideBanUserDialog,
- notify,
} = this.props;
- try {
- await banUser({ id: userId, message: '' });
- hideBanUserDialog();
- if (commentId && commentStatus && commentStatus !== 'REJECTED') {
- await setCommentStatus({ commentId, status: 'REJECTED' });
- }
- } catch (err) {
- notify('error', getErrorMessages(err));
+ await banUser({ id: userId, message: '' });
+ hideBanUserDialog();
+ if (commentId && commentStatus && commentStatus !== 'REJECTED') {
+ await setCommentStatus({ commentId, status: 'REJECTED' });
}
};
@@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
- notify,
},
dispatch
),
});
export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
withBanUser,
- withSetCommentStatus,
- connect(mapStateToProps, mapDispatchToProps)
+ withSetCommentStatus
)(BanUserDialogContainer);
diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js
index e058faad6..f03d48f52 100644
--- a/client/coral-admin/src/containers/SuspendUserDialog.js
+++ b/client/coral-admin/src/containers/SuspendUserDialog.js
@@ -11,7 +11,6 @@ import {
import { compose, gql } from 'react-apollo';
import t, { timeago } from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
-import { getErrorMessages } from 'coral-framework/utils';
import get from 'lodash/get';
import { notify } from 'coral-framework/actions/notification';
@@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component {
notify,
} = this.props;
hideSuspendUserDialog();
- try {
- await suspendUser({ id: userId, message, until });
- notify(
- 'success',
- t('suspenduser.notify_suspend_until', username, timeago(until))
- );
- if (commentId && commentStatus && commentStatus !== 'REJECTED') {
- await setCommentStatus({ commentId, status: 'REJECTED' });
- }
- } catch (err) {
- notify('error', getErrorMessages(err));
+ await suspendUser({ id: userId, message, until });
+ notify(
+ 'success',
+ t('suspenduser.notify_suspend_until', username, timeago(until))
+ );
+ if (commentId && commentStatus && commentStatus !== 'REJECTED') {
+ await setCommentStatus({ commentId, status: 'REJECTED' });
}
};
diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js
index bd34ac4d8..fb1508a2f 100644
--- a/client/coral-admin/src/containers/UserDetail.js
+++ b/client/coral-admin/src/containers/UserDetail.js
@@ -24,7 +24,6 @@ import {
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
-import { notify } from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
@@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component {
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
+ error={this.props.data && this.props.data.error}
loadMore={this.loadMore}
{...this.props}
/>
@@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
- notify,
},
dispatch
),
diff --git a/client/coral-admin/src/routes/Community/components/FlaggedUser.css b/client/coral-admin/src/routes/Community/components/FlaggedUser.css
index 75a91ce29..436f7307d 100644
--- a/client/coral-admin/src/routes/Community/components/FlaggedUser.css
+++ b/client/coral-admin/src/routes/Community/components/FlaggedUser.css
@@ -10,7 +10,7 @@
margin: 0 auto;
position: relative;
transition: background 200ms, box-shadow 200ms, margin-bottom 200ms;
- padding: 10px 0 0;
+ padding: 10px 0 10px;
min-height: 220px;
&:hover {
diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
index ddd684f87..60ae4ee99 100644
--- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
+++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
@@ -18,7 +18,6 @@ import { handleFlaggedAccountsChange } from '../graphql';
import { notify } from 'coral-framework/actions/notification';
import { isFlaggedUserDangling } from '../utils';
import t from 'coral-framework/services/i18n';
-import { notifyOnMutationError } from 'coral-framework/hocs';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
@@ -300,7 +299,6 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(null, mapDispatchToProps),
withApproveUsername,
- notifyOnMutationError(['approveUsername']),
withQuery(
gql`
query TalkAdmin_Community_FlaggedAccounts {
diff --git a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
index f9ada0945..325eadfb5 100644
--- a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
+++ b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
@@ -4,8 +4,6 @@ import { hideRejectUsernameDialog } from '../../../actions/community';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose } from 'react-apollo';
-import { notify } from 'coral-framework/actions/notification';
-import { notifyOnMutationError } from 'coral-framework/hocs';
const mapStateToProps = state => ({
user: state.community.user,
@@ -16,13 +14,11 @@ const mapDispatchToProps = dispatch =>
bindActionCreators(
{
handleClose: hideRejectUsernameDialog,
- notify,
},
dispatch
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
- withRejectUsername,
- notifyOnMutationError(['rejectUsername'])
+ withRejectUsername
)(RejectUsernameDialog);
diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js
index 0d9d5faf9..ce33fa1f4 100644
--- a/client/coral-admin/src/routes/Configure/containers/Configure.js
+++ b/client/coral-admin/src/routes/Configure/containers/Configure.js
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withQuery, withMergedSettings } from 'coral-framework/hocs';
import { Spinner } from 'coral-ui';
-import { notify } from 'coral-framework/actions/notification';
import PropTypes from 'prop-types';
import { withUpdateSettings } from 'coral-framework/graphql/mutations';
-import { getErrorMessages, getDefinitionName } from 'coral-framework/utils';
+import { getDefinitionName } from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
@@ -16,22 +15,21 @@ import Configure from '../components/Configure';
class ConfigureContainer extends Component {
savePending = async () => {
- try {
- await this.props.updateSettings(this.props.pending);
- this.props.clearPending();
- } catch (err) {
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.updateSettings(this.props.pending);
+ this.props.clearPending();
};
render() {
+ if (this.props.data.error) {
+ return
{this.props.data.error.message}
;
+ }
+
if (this.props.data.loading) {
return ;
}
return (
({
variables: {},
+ fetchPolicy: 'network-only',
}),
}
);
@@ -81,7 +80,6 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
- notify,
clearPending,
setActiveSection,
},
@@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
withUpdateSettings,
withConfigureQuery,
- connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('root.settings', 'pending', 'mergedSettings')
)(ConfigureContainer);
@@ -99,7 +97,6 @@ ConfigureContainer.propTypes = {
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
- notify: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
index b5820287b..c0e2e60a6 100644
--- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js
@@ -292,10 +292,6 @@ class ModerationContainer extends Component {
const { root, root: { asset, settings }, data } = this.props;
const assetId = getAssetId(this.props);
- if (data.error) {
- return Error
;
- }
-
if (assetId) {
if (asset === null) {
// Not found.
@@ -303,6 +299,10 @@ class ModerationContainer extends Component {
}
}
+ if (data.error) {
+ return {data.error.message}
;
+ }
+
if (data.loading && data.networkStatus !== 3) {
// loading.
return ;
diff --git a/client/coral-auth-callback/src/index.js b/client/coral-auth-callback/src/index.js
new file mode 100644
index 000000000..b7f3ed226
--- /dev/null
+++ b/client/coral-auth-callback/src/index.js
@@ -0,0 +1,14 @@
+document.addEventListener('DOMContentLoaded', () => {
+ // Get the auth element and parse it as JSON by decoding it.
+ const auth = document.getElementById('auth');
+ const doc = document.implementation.createHTMLDocument('');
+ doc.body.innerHTML = auth.innerText;
+
+ // Set the item in localStorage.
+ localStorage.setItem('auth', doc.body.textContent);
+
+ // Close the window.
+ setTimeout(() => {
+ window.close();
+ }, 50);
+});
diff --git a/client/coral-embed-stream/src/tabs/configure/components/Settings.js b/client/coral-embed-stream/src/tabs/configure/components/Settings.js
index 2c4a42153..7e11b1311 100644
--- a/client/coral-embed-stream/src/tabs/configure/components/Settings.js
+++ b/client/coral-embed-stream/src/tabs/configure/components/Settings.js
@@ -57,7 +57,7 @@ class Settings extends React.Component {
{this.props.data.error.message};
+ }
+
return (
{
- try {
- await this.props.updateAssetSettings(
- this.props.asset.id,
- this.props.pending
- );
- this.props.clearPending();
- } catch (err) {
- this.props.notify('error', getErrorMessages(err));
- }
+ await this.props.updateAssetSettings(
+ this.props.asset.id,
+ this.props.pending
+ );
+ this.props.clearPending();
};
render() {
@@ -98,7 +90,6 @@ SettingsContainer.propTypes = {
mergedSettings: PropTypes.object.isRequired,
updateAssetSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
- notify: PropTypes.func.isRequired,
updatePending: PropTypes.func.isRequired,
canSave: PropTypes.bool.isRequired,
};
@@ -135,7 +126,6 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
- notify,
clearPending,
updatePending,
},
@@ -143,9 +133,9 @@ const mapDispatchToProps = dispatch =>
);
const enhance = compose(
+ connect(mapStateToProps, mapDispatchToProps),
withSettingsFragments,
withUpdateAssetSettings,
- connect(mapStateToProps, mapDispatchToProps),
withMergedSettings('asset.settings', 'pending', 'mergedSettings')
);
diff --git a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
index ed1fe73f9..3190ab155 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/AllCommentsPane.js
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
import { TransitionGroup } from 'react-transition-group';
-import { forEachError } from 'coral-framework/utils';
import Comment from '../containers/Comment';
import NoComments from './NoComments';
@@ -91,11 +90,8 @@ class AllCommentsPane extends React.Component {
.then(() => {
this.setState({ loadingState: 'success' });
})
- .catch(error => {
+ .catch(() => {
this.setState({ loadingState: 'error' });
- forEachError(error, ({ msg }) => {
- this.props.notify('error', msg);
- });
});
};
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
index f4e8404e4..cd0fab9e9 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
@@ -24,7 +24,6 @@ import { EditableCommentContent } from './EditableCommentContent';
import {
getActionSummary,
iPerformedThisAction,
- forEachError,
isCommentActive,
getShallowChanges,
} from 'coral-framework/utils';
@@ -261,11 +260,8 @@ export default class Comment extends React.Component {
loadingState: 'success',
});
})
- .catch(error => {
+ .catch(() => {
this.setState({ loadingState: 'error' });
- forEachError(error, ({ msg }) => {
- this.props.notify('error', msg);
- });
});
emit('ui.Comment.showMoreReplies', { id });
return;
diff --git a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js
index bfd2c8bad..f4cfc42e8 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/EditableCommentContent.js
@@ -6,7 +6,6 @@ import styles from './Comment.css';
import { CountdownSeconds } from './CountdownSeconds';
import { getEditableUntilDate } from './util';
import { can } from 'coral-framework/services/perms';
-import { forEachError } from 'coral-framework/utils';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
@@ -80,7 +79,7 @@ export class EditableCommentContent extends React.Component {
this.setState({ loadingState: 'loading' });
- const { editComment, notify, stopEditing } = this.props;
+ const { editComment, stopEditing } = this.props;
if (typeof editComment !== 'function') {
return;
}
@@ -95,7 +94,6 @@ export class EditableCommentContent extends React.Component {
}
} catch (error) {
this.setState({ loadingState: 'error' });
- forEachError(error, ({ msg }) => notify('error', msg));
}
};
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
index d82f49498..6288a2b6b 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { StreamError } from './StreamError';
+import StreamError from './StreamError';
import Comment from '../containers/Comment';
import BannedAccount from '../../../components/BannedAccount';
import ChangeUsername from '../containers/ChangeUsername';
diff --git a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js
index 0955de2ef..79d6b03fc 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/StreamError.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/StreamError.js
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './StreamError.css';
-export const StreamError = ({ children }) => (
+export default ({ children }) => (
{children}
);
diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
index 70cbadae5..51b4d9d9e 100644
--- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
@@ -38,6 +38,7 @@ import {
insertFetchedCommentsIntoEmbedQuery,
nest,
} from '../../../graphql/utils';
+import StreamError from '../components/StreamError';
const { showSignInDialog, editName } = authActions;
const { notify } = notificationActions;
@@ -208,6 +209,10 @@ class StreamContainer extends React.Component {
}
render() {
+ if (this.props.data.error) {
+ return {this.props.data.error.message};
+ }
+
if (
!this.props.asset ||
(this.props.asset.comment === undefined && !this.props.asset.comments)
@@ -424,7 +429,8 @@ export default compose(
withEmit,
connect(mapStateToProps, mapDispatchToProps),
withPostComment,
- withPostFlag,
+ // `talk-plugin-flags` has a custom error handling logic.
+ withPostFlag({ notifyOnError: false }),
withPostDontAgree,
withDeleteAction,
withEditComment
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index 275f6df42..174f25a63 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -6,4 +6,3 @@ export { default as withEmit } from './withEmit';
export { default as excludeIf } from './excludeIf';
export { default as connect } from './connect';
export { default as withMergedSettings } from './withMergedSettings';
-export { default as notifyOnMutationError } from './notifyOnMutationError';
diff --git a/client/coral-framework/hocs/notifyOnMutationError.js b/client/coral-framework/hocs/notifyOnMutationError.js
deleted file mode 100644
index c9429ad15..000000000
--- a/client/coral-framework/hocs/notifyOnMutationError.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { compose } from 'react-apollo';
-import { notify } from 'coral-framework/actions/notification';
-import { forEachError } from 'coral-framework/utils';
-import { withProps } from 'recompose';
-
-const notifyOnMutationError = keys =>
- compose(
- connect(null, dispatch =>
- bindActionCreators(
- {
- notify,
- },
- dispatch
- )
- ),
- withProps(ownProps =>
- keys.reduce((props, key) => {
- props[key] = async (...args) => {
- try {
- return await ownProps[key](...args);
- } catch (e) {
- forEachError(e, ({ msg }) => {
- ownProps.notify('error', msg);
- });
- throw e;
- }
- };
- return props;
- }, {})
- )
- );
-
-export default notifyOnMutationError;
diff --git a/client/coral-framework/hocs/withMutation.js b/client/coral-framework/hocs/withMutation.js
index b72e56f40..dae38071f 100644
--- a/client/coral-framework/hocs/withMutation.js
+++ b/client/coral-framework/hocs/withMutation.js
@@ -4,11 +4,16 @@ import merge from 'lodash/merge';
import uniq from 'lodash/uniq';
import flatten from 'lodash/flatten';
import isEmpty from 'lodash/isEmpty';
-import { getDefinitionName, getResponseErrors } from '../utils';
+import {
+ getDefinitionName,
+ getResponseErrors,
+ getErrorMessages,
+} from '../utils';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import hoistStatics from 'recompose/hoistStatics';
import union from 'lodash/union';
+import { notify } from 'coral-framework/actions/notification';
class ResponseErrors extends Error {
constructor(errors) {
@@ -27,11 +32,7 @@ class ResponseError {
}
}
-/**
- * Exports a HOC with the same signature as `graphql`, that will
- * apply mutation options registered in the graphRegistry.
- */
-export default (document, config = {}) =>
+const createHOC = (document, config, { notifyOnError = true }) =>
hoistStatics(WrappedComponent => {
config = {
...config,
@@ -46,10 +47,18 @@ export default (document, config = {}) =>
graphql: PropTypes.object,
};
+ static propTypes = {
+ notify: PropTypes.func,
+ };
+
get graphqlRegistry() {
return this.context.graphql.registry;
}
+ notifyErrors(messages) {
+ this.context.store.dispatch(notify('error', messages));
+ }
+
resolveDocument(documentOrCallback) {
return this.context.graphql.resolveDocument(
documentOrCallback,
@@ -165,6 +174,11 @@ export default (document, config = {}) =>
variables,
error,
});
+
+ // Show errors as notifications.
+ if (notifyOnError) {
+ this.notifyErrors(getErrorMessages(error));
+ }
throw error;
});
};
@@ -213,3 +227,18 @@ export default (document, config = {}) =>
}
};
});
+
+/**
+ * Exports a HOC with the same signature as `graphql`, that will
+ * apply mutation options registered in the graphRegistry.
+ *
+ * The returned HOC accepts a settings object with the following properties:
+ * notifyOnError: show a notification to the user when an error occured.
+ * Defaults to true.
+ */
+export default (document, config = {}) => settingsOrComponent => {
+ if (typeof settingsOrComponent === 'function') {
+ return createHOC(document, config, {})(settingsOrComponent);
+ }
+ return createHOC(document, config, settingsOrComponent);
+};
diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js
index d5609c340..c4b9074b4 100644
--- a/client/coral-framework/hocs/withQuery.js
+++ b/client/coral-framework/hocs/withQuery.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import hoistStatics from 'recompose/hoistStatics';
import { getOperationName } from 'apollo-client/queries/getFromAST';
import throttle from 'lodash/throttle';
+import get from 'lodash/get';
+import { notify } from 'coral-framework/actions/notification';
const withSkipOnErrors = reducer => (prev, action, ...rest) => {
if (
@@ -36,21 +38,23 @@ function networkStatusToString(networkStatus) {
return 'ready';
case 8:
return 'error';
+ default:
+ throw new Error(`Unknown network status ${networkStatus}`);
}
- throw new Error(`Unknown network status ${networkStatus}`);
}
-/**
- * Exports a HOC with the same signature as `graphql`, that will
- * apply query options registered in the graphRegistry.
- */
-export default (document, config = {}) =>
+const createHOC = (document, config, { notifyOnError = true }) =>
hoistStatics(WrappedComponent => {
return class WithQuery extends React.Component {
static contextTypes = {
eventEmitter: PropTypes.object,
graphql: PropTypes.object,
client: PropTypes.object,
+ store: PropTypes.object,
+ };
+
+ static propTypes = {
+ notify: PropTypes.func,
};
// Lazily resolve fragments from graphRegistry to support circular dependencies.
@@ -166,10 +170,24 @@ export default (document, config = {}) =>
return () => this.client.networkInterface.unsubscribe(id);
};
+ notifyErrors(messages) {
+ this.context.store.dispatch(notify('error', messages));
+ }
+
nextData(data) {
this.apolloData = data;
this.emitWhenNeeded(data);
+ if (
+ get(data, 'error.message') &&
+ get(this, 'data.error.message') !== get(data, 'error.message')
+ ) {
+ // Show errors as notifications.
+ if (notifyOnError) {
+ this.notifyErrors(data.error.message);
+ }
+ }
+
// If data was previously set, we update it in a immutable way.
if (this.data) {
if (this.data.loading && !data.loading) {
@@ -319,3 +337,18 @@ export default (document, config = {}) =>
}
};
});
+
+/**
+ * Exports a HOC with the same signature as `graphql`, that will
+ * apply query options registered in the graphRegistry.
+ *
+ * The returned HOC accepts a settings object with the following properties:
+ * notifyOnError: show a notification to the user when an error occured.
+ * Defaults to true.
+ */
+export default (document, config = {}) => settingsOrComponent => {
+ if (typeof settingsOrComponent === 'function') {
+ return createHOC(document, config, {})(settingsOrComponent);
+ }
+ return createHOC(document, config, settingsOrComponent);
+};
diff --git a/client/coral-settings/containers/ProfileContainer.js b/client/coral-settings/containers/ProfileContainer.js
index fb9d8de17..e1bcf7f13 100644
--- a/client/coral-settings/containers/ProfileContainer.js
+++ b/client/coral-settings/containers/ProfileContainer.js
@@ -65,6 +65,10 @@ class ProfileContainer extends Component {
const { me } = this.props.root;
const loading = this.props.data.loading;
+ if (this.props.data.error) {
+ return {this.props.data.error.message}
;
+ }
+
if (!auth.loggedIn) {
return ;
}
diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js
index a8da78f3f..8eac1ff2f 100644
--- a/client/talk-plugin-commentbox/CommentBox.js
+++ b/client/talk-plugin-commentbox/CommentBox.js
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
-import { forEachError } from 'coral-framework/utils';
import Slot from 'coral-framework/components/Slot';
import { connect } from 'react-redux';
@@ -93,9 +92,8 @@ class CommentBox extends React.Component {
commentPostedHandler();
}
})
- .catch(err => {
+ .catch(() => {
this.setState({ loadingState: 'error' });
- forEachError(err, ({ msg }) => notify('error', msg));
});
};
diff --git a/client/talk-plugin-flags/components/FlagButton.js b/client/talk-plugin-flags/components/FlagButton.js
index 64841a893..b2048e105 100644
--- a/client/talk-plugin-flags/components/FlagButton.js
+++ b/client/talk-plugin-flags/components/FlagButton.js
@@ -60,9 +60,10 @@ export default class FlagButton extends Component {
});
};
- onPopupContinue = () => {
+ onPopupContinue = async () => {
const { postFlag, postDontAgree, id, author_id } = this.props;
const { itemType, reason, step, message } = this.state;
+ let failed = false;
switch (step) {
case 0:
@@ -75,13 +76,9 @@ export default class FlagButton extends Component {
return;
}
break;
- }
-
- // Proceed to the next step or close the menu if we've reached the end
- if (step + 1 >= this.props.getPopupMenu.length) {
- this.closeMenu();
- } else {
- this.setState({ step: step + 1 });
+ case this.props.getPopupMenu.length:
+ this.closeMenu();
+ return;
}
// If itemType and reason are both set, post the action
@@ -96,43 +93,46 @@ export default class FlagButton extends Component {
break;
}
- if (itemType === 'COMMENTS') {
- this.setState({ localPost: 'temp' });
- }
-
let action = {
item_id,
item_type: itemType,
message,
};
+
if (reason === REASONS.comment.noagree) {
- postDontAgree(action)
- .then(({ data }) => {
- if (itemType === 'COMMENTS') {
- this.setState({ localPost: data.createDontAgree.dontagree.id });
- }
- })
- .catch(err => {
- this.props.notify('error', getErrorMessages(err));
- console.error(err);
- });
- } else {
- postFlag({ ...action, reason })
- .then(({ data }) => {
- if (itemType === 'COMMENTS') {
- this.setState({ localPost: data.createFlag.flag.id });
- }
- })
- .catch(errors => {
- forEachError(errors, ({ error, msg }) => {
- if (error.translation_key === 'ALREADY_EXISTS') {
- msg = t('already_flagged_username');
- }
- this.props.notify('error', msg);
+ const result = await postDontAgree(action);
+ try {
+ if (itemType === 'COMMENTS') {
+ this.setState({
+ localPost: result.data.createDontAgree.dontagree.id,
});
+ }
+ } catch (err) {
+ this.props.notify('error', getErrorMessages(err));
+ console.error(err);
+ failed = true;
+ }
+ } else {
+ try {
+ const result = await postFlag({ ...action, reason });
+ if (itemType === 'COMMENTS') {
+ this.setState({ localPost: result.data.createFlag.flag.id });
+ }
+ } catch (errors) {
+ forEachError(errors, ({ error, msg }) => {
+ if (error.translation_key === 'ALREADY_EXISTS') {
+ msg = t('already_flagged_username');
+ }
+ this.props.notify('error', msg);
});
+ failed = true;
+ }
}
}
+
+ if (!failed) {
+ this.setState({ step: step + 1 });
+ }
};
onPopupOptionClick = sets => e => {
diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js
index a56350c4a..a17a347c5 100644
--- a/client/talk-plugin-history/CommentHistory.js
+++ b/client/talk-plugin-history/CommentHistory.js
@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import Comment from './Comment';
import LoadMore from './LoadMore';
-import { forEachError } from 'plugin-api/beta/client/utils';
class CommentHistory extends React.Component {
state = {
@@ -16,11 +15,8 @@ class CommentHistory extends React.Component {
.then(() => {
this.setState({ loadingState: 'success' });
})
- .catch(error => {
+ .catch(() => {
this.setState({ loadingState: 'error' });
- forEachError(error, ({ msg }) => {
- this.props.notify('error', msg);
- });
});
};
@@ -55,7 +51,6 @@ class CommentHistory extends React.Component {
CommentHistory.propTypes = {
comments: PropTypes.object.isRequired,
loadMore: PropTypes.func,
- notify: PropTypes.func,
link: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
diff --git a/graph/connectors.js b/graph/connectors.js
index a27b9d38c..976f05b71 100644
--- a/graph/connectors.js
+++ b/graph/connectors.js
@@ -24,6 +24,7 @@ const Limit = require('../services/limit');
const Mailer = require('../services/mailer');
const Metadata = require('../services/metadata');
const Migration = require('../services/migration');
+const Moderation = require('../services/moderation');
const Mongoose = require('../services/mongoose');
const Passport = require('../services/passport');
const Plugins = require('../services/plugins');
@@ -62,6 +63,7 @@ const connectors = {
Mailer,
Metadata,
Migration,
+ Moderation,
Mongoose,
Passport,
Plugins,
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 6106f819c..7e0607287 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -1,13 +1,11 @@
const errors = require('../../errors');
const ActionModel = require('../../models/action');
-const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const TagsService = require('../../services/tags');
const CommentsService = require('../../services/comments');
const KarmaService = require('../../services/karma');
const merge = require('lodash/merge');
-const linkify = require('linkify-it')().tlds(require('tlds'));
-const Wordlist = require('../../services/wordlist');
+
const {
CREATE_COMMENT,
SET_COMMENT_STATUS,
@@ -15,10 +13,6 @@ const {
EDIT_COMMENT,
} = require('../../perms/constants');
const debug = require('debug')('talk:graph:mutators:comment');
-const {
- DISABLE_AUTOFLAG_SUSPECT_WORDS,
- IGNORE_FLAGS_AGAINST_STAFF,
-} = require('../../config');
const resolveTagsForComment = async (
{ user, loaders: { Tags } },
@@ -188,279 +182,27 @@ const createComment = async (
return comment;
};
-/**
- * Filters the comment object and outputs wordlist results.
- * @param {Object} context graphql context
- * @param {String} body body of a comment
- * @param {String} [asset_id] id of asset comment is posted on
- * @return {Object} resolves to the wordlist results
- */
-const filterNewComment = async (context, { body, asset_id }) => {
- // Load the settings.
- const [settings, asset] = await Promise.all([
- context.loaders.Settings.load(),
- context.loaders.Assets.getByID.load(asset_id),
- ]);
-
- // Create a new instance of the Wordlist.
- const wl = new Wordlist();
-
- // Load the wordlist.
- wl.upsert(settings.wordlist);
-
- // Load the wordlist and filter the comment content.
- return [
- // Scan the word.
- wl.scan('body', body),
-
- // Return the asset's settings.
- await AssetsService.rectifySettings(asset, settings),
- ];
-};
-
-/**
- * moderationPhases is an array of phases carried out in order until a status is
- * returned.
- */
-const moderationPhases = [
- // This phase checks to see if the comment is long enough.
- (context, comment) => {
- // Check to see if the body is too short, if it is, then complain about it!
- if (comment.body.length < 2) {
- throw errors.ErrCommentTooShort;
- }
- },
-
- // This phase checks to see if the asset being processed is closed or not.
- (context, comment, { asset }) => {
- // Check to see if the asset has closed commenting...
- if (asset.isClosed) {
- throw new errors.ErrAssetCommentingClosed(asset.closedMessage);
- }
- },
-
- // This phase checks the comment against the wordlist.
- (context, comment, { wordlist }) => {
- // Decide the status based on whether or not the current asset/settings
- // has pre-mod enabled or not. If the comment was rejected based on the
- // wordlist, then reject it, otherwise if the moderation setting is
- // premod, set it to `premod`.
- if (wordlist.banned) {
- // Add the flag related to Trust to the comment.
- return {
- status: 'REJECTED',
- actions: [
- {
- action_type: 'FLAG',
- user_id: null,
- group_id: 'BANNED_WORD',
- metadata: {},
- },
- ],
- };
- }
-
- // If the comment has a suspect word or a link, we need to add a
- // flag to it to indicate that it needs to be looked at.
- // Otherwise just return the new comment.
-
- // If the wordlist has matched the suspect word filter and we haven't disabled
- // auto-flagging suspect words, then we should flag the comment!
- if (wordlist.suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
- // TODO: this is kind of fragile, we should refactor this to resolve
- // all these const's that we're using like 'COMMENTS', 'FLAG' to be
- // defined in a checkable schema.
- return {
- actions: [
- {
- action_type: 'FLAG',
- user_id: null,
- group_id: 'SUSPECT_WORD',
- metadata: {},
- },
- ],
- };
- }
- },
-
- // This phase checks to see if the comment's length exceeds maximum.
- (context, comment, { assetSettings: { charCountEnable, charCount } }) => {
- // Reject if the comment is too long
- if (charCountEnable && comment.body.length > charCount) {
- // Add the flag related to Trust to the comment.
- return {
- status: 'REJECTED',
- actions: [
- {
- action_type: 'FLAG',
- user_id: null,
- group_id: 'BODY_COUNT',
- metadata: {
- count: comment.body.length,
- },
- },
- ],
- };
- }
- },
-
- // If a given user is a staff member, always approve their comment.
- context => {
- if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) {
- return {
- status: 'ACCEPTED',
- };
- }
- },
-
- // This phase checks the comment if it has any links in it if the check is
- // enabled.
- (context, comment, { assetSettings: { premodLinksEnable } }) => {
- if (premodLinksEnable && linkify.test(comment.body)) {
- // Add the flag related to Trust to the comment.
- return {
- status: 'SYSTEM_WITHHELD',
- actions: [
- {
- action_type: 'FLAG',
- user_id: null,
- group_id: 'LINKS',
- metadata: {
- links: comment.body,
- },
- },
- ],
- };
- }
- },
-
- // This phase checks to see if the user making the comment is allowed to do so
- // considering their reliability (Trust) status.
- context => {
- if (context.user && context.user.metadata) {
- // If the user is not a reliable commenter (passed the unreliability
- // threshold by having too many rejected comments) then we can change the
- // status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's
- // comments away from the public eye until a moderator can manage them. This of
- // course can only be applied if the comment's current status is `NONE`,
- // we don't want to interfere if the comment was rejected.
- if (
- KarmaService.isReliable('comment', context.user.metadata.trust) ===
- false
- ) {
- // Add the flag related to Trust to the comment.
- return {
- status: 'SYSTEM_WITHHELD',
- actions: [
- {
- action_type: 'FLAG',
- user_id: null,
- group_id: 'TRUST',
- metadata: {
- trust: context.user.metadata.trust,
- },
- },
- ],
- };
- }
- }
- },
-
- // This phase checks to see if the comment was already prescribed a status.
- (context, comment) => {
- // If the status was already defined, don't redefine it. It's only defined
- // when specific external conditions exist, we don't want to override that.
- if (comment.status && comment.status.length > 0) {
- return {
- status: comment.status,
- };
- }
- },
-
- // This phase checks to see if the settings have premod enabled, if they do,
- // the comment is premod, otherwise, it's just none.
- (context, comment, { assetSettings: { moderation } }) => {
- // If the settings say that we're in premod mode, then the comment is in
- // premod status.
- if (moderation === 'PRE') {
- return {
- status: 'PREMOD',
- };
- }
-
- return {
- status: 'NONE',
- };
- },
-];
-
-/**
- * This resolves a given comment's status and actions.
- * @param {Object} context graphql context
- * @param {String} body body of the comment
- * @param {String} [asset_id] asset for the comment
- * @param {Object} [wordlist={}] the results of the wordlist scan
- * @return {Promise} resolves to the comment's status and actions
- */
-const resolveCommentModeration = async (context, comment) => {
- // First we filter the comment contents to ensure that we note any validation
- // issues.
- let [wordlist, settings] = await filterNewComment(context, comment);
-
- // Get the asset from the loader.
- const asset = await context.loaders.Assets.getByID.load(comment.asset_id);
- if (!asset) {
- // And leave now if this asset wasn't found.
- throw errors.ErrNotFound;
- }
-
- // Combine the asset and the settings to get the asset settings.
- const assetSettings = await AssetsService.rectifySettings(asset, settings);
-
- let actions = comment.actions || [];
-
- // Loop over all the moderation phases and see if we've resolved the status.
- for (const phase of moderationPhases) {
- const result = await phase(context, comment, {
- asset,
- assetSettings,
- settings,
- wordlist,
- });
-
- if (result) {
- if (result.actions) {
- actions.push(...result.actions);
- }
-
- // If this result contained a status, then we've finished resolving
- // phases!
- if (result.status) {
- return { status: result.status, actions };
- }
- }
- }
-};
-
/**
* createPublicComment is designed to create a comment from a public source. It
* validates the comment, and performs some automated moderator actions based on
* the settings.
- * @param {Object} context the graphql context
+ * @param {Object} ctx the graphql context
* @param {Object} commentInput the new comment to be created
* @return {Promise} resolves to a new comment
*/
-const createPublicComment = async (context, comment) => {
+const createPublicComment = async (ctx, comment) => {
+ const { connectors: { services: { Moderation } } } = ctx;
+
// We then take the wordlist and the comment into consideration when
// considering what status to assign the new comment, and resolve the new
// status to set the comment to.
- let { actions, status } = await resolveCommentModeration(context, comment);
+ let { actions, status } = await Moderation.process(ctx, comment);
// Assign status to comment.
comment.status = status;
// Then we actually create the comment with the new status.
- const result = await createComment(context, comment);
+ const result = await createComment(ctx, comment);
// Create all the actions that were determined during the moderation check
// phase.
@@ -522,18 +264,20 @@ const setStatus = async ({ user, loaders: { Comments } }, { id, status }) => {
* @param {Object} edit describes how to edit the comment
* @param {String} edit.body the new Comment body
*/
-const edit = async (context, { id, asset_id, edit: { body } }) => {
+const edit = async (ctx, { id, asset_id, edit: { body } }) => {
+ const { connectors: { services: { Moderation } } } = ctx;
+
// Build up the new comment we're setting. We need to check this with
// moderation now.
let comment = { id, asset_id, body };
// Determine the new status of the comment.
- const { actions, status } = await resolveCommentModeration(context, comment);
+ const { actions, status } = await Moderation.process(ctx, comment);
// Execute the edit.
comment = await CommentsService.edit({
id,
- author_id: context.user.id,
+ author_id: ctx.user.id,
body,
status,
});
@@ -543,7 +287,7 @@ const edit = async (context, { id, asset_id, edit: { body } }) => {
await createActions(comment.id, actions);
// Publish the edited comment via the subscription.
- context.pubsub.publish('commentEdited', comment);
+ ctx.pubsub.publish('commentEdited', comment);
return comment;
};
diff --git a/models/asset.js b/models/asset.js
index 68ca08bbf..6fdea3b78 100644
--- a/models/asset.js
+++ b/models/asset.js
@@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose');
const Schema = mongoose.Schema;
const uuid = require('uuid');
const TagLinkSchema = require('./schema/tag_link');
+const get = require('lodash/get');
const AssetSchema = new Schema(
{
@@ -45,8 +46,8 @@ const AssetSchema = new Schema(
// the base settings from the base Settings object. This is to be accessed
// always after running `rectifySettings` against it.
settings: {
- type: Schema.Types.Mixed,
default: {},
+ type: Object,
},
// Tags are added by the self or by administrators.
@@ -85,9 +86,12 @@ AssetSchema.index(
* Returns true if the asset is closed, false else.
*/
AssetSchema.virtual('isClosed').get(function() {
- return Boolean(
- this.closedAt && this.closedAt.getTime() <= new Date().getTime()
- );
+ const closedAt = get(this, 'closedAt', null);
+ if (closedAt === null) {
+ return false;
+ }
+
+ return closedAt.getTime() <= new Date().getTime();
});
const Asset = mongoose.model('Asset', AssetSchema);
diff --git a/package.json b/package.json
index 90abdf2e7..17af19867 100644
--- a/package.json
+++ b/package.json
@@ -211,7 +211,7 @@
"mocha-junit-reporter": "^1.12.1",
"nightwatch": "^0.9.16",
"nodemon": "^1.11.0",
- "pre-git": "^3.16.0",
+ "pre-commit": "^1.2.2",
"selenium-standalone": "^6.11.0",
"sinon": "^3.2.1",
"sinon-chai": "^2.13.0",
@@ -220,24 +220,8 @@
"engines": {
"node": "^8"
},
- "config": {
- "pre-git": {
- "pre-commit": [
- "yarn lint",
- "yarn test:client",
- "yarn test:server"
- ],
- "pre-push": [
- "yarn lint",
- "yarn test:client",
- "yarn test:server"
- ],
- "post-commit": [],
- "post-checkout": [],
- "post-merge": []
- }
- },
- "release": {
- "analyzeCommits": "simple-commit-message"
+ "pre-commit": {
+ "silent": false,
+ "run": ["lint", "test:client", "test:server"]
}
}
diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js
index f5a0167b7..6020d4002 100644
--- a/plugin-api/beta/client/hocs/withReaction.js
+++ b/plugin-api/beta/client/hocs/withReaction.js
@@ -9,11 +9,7 @@ import withFragments from 'coral-framework/hocs/withFragments';
import withMutation from 'coral-framework/hocs/withMutation';
import { notify } from 'coral-framework/actions/notification';
import { capitalize } from 'coral-framework/helpers/strings';
-import {
- getMyActionSummary,
- getTotalActionCount,
- getErrorMessages,
-} from 'coral-framework/utils';
+import { getMyActionSummary, getTotalActionCount } from 'coral-framework/utils';
import hoistStatics from 'recompose/hoistStatics';
import * as PropTypes from 'prop-types';
import { getDefinitionName } from '../utils';
@@ -282,7 +278,6 @@ export default (reaction, options = {}) =>
})
.catch(err => {
this.duringMutation = false;
- this.props.notify('error', getErrorMessages(err));
throw err;
});
};
@@ -307,7 +302,6 @@ export default (reaction, options = {}) =>
})
.catch(err => {
this.duringMutation = false;
- this.props.notify('error', getErrorMessages(err));
throw err;
});
};
diff --git a/plugin-api/beta/client/hocs/withTags.js b/plugin-api/beta/client/hocs/withTags.js
index 7b95a5e6c..958028a4b 100644
--- a/plugin-api/beta/client/hocs/withTags.js
+++ b/plugin-api/beta/client/hocs/withTags.js
@@ -1,13 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import { capitalize } from 'coral-framework/helpers/strings';
import { withAddTag, withRemoveTag } from 'coral-framework/graphql/mutations';
import withFragments from 'coral-framework/hocs/withFragments';
-import { notify } from 'coral-framework/actions/notification';
-import { getErrorMessages, isTagged } from 'coral-framework/utils';
+import { isTagged } from 'coral-framework/utils';
import hoistStatics from 'recompose/hoistStatics';
import { getDefinitionName } from '../utils';
@@ -38,7 +36,7 @@ export default (tag, options = {}) =>
loading = false;
postTag = () => {
- const { comment, asset, notify } = this.props;
+ const { comment, asset } = this.props;
if (this.loading) {
return;
@@ -59,13 +57,12 @@ export default (tag, options = {}) =>
})
.catch(err => {
this.loading = false;
- notify('error', getErrorMessages(err));
throw err;
});
};
deleteTag = () => {
- const { comment, asset, notify } = this.props;
+ const { comment, asset } = this.props;
if (this.loading) {
return;
@@ -84,7 +81,6 @@ export default (tag, options = {}) =>
})
.catch(err => {
this.loading = false;
- notify('error', getErrorMessages(err));
throw err;
});
};
@@ -114,9 +110,6 @@ export default (tag, options = {}) =>
user: state.auth.user,
});
- const mapDispatchToProps = dispatch =>
- bindActionCreators({ notify }, dispatch);
-
const enhance = compose(
withFragments({
...fragments,
@@ -146,7 +139,7 @@ export default (tag, options = {}) =>
}),
withAddTag,
withRemoveTag,
- connect(mapStateToProps, mapDispatchToProps)
+ connect(mapStateToProps, null)
);
WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`;
diff --git a/plugins/talk-plugin-featured-comments/client/components/ModTag.js b/plugins/talk-plugin-featured-comments/client/components/ModTag.js
index 420ee9417..659b2cea4 100644
--- a/plugins/talk-plugin-featured-comments/client/components/ModTag.js
+++ b/plugins/talk-plugin-featured-comments/client/components/ModTag.js
@@ -64,7 +64,6 @@ export default class ModTag extends React.Component {
ModTag.propTypes = {
alreadyTagged: PropTypes.bool,
deleteTag: PropTypes.func,
- notify: PropTypes.func,
openFeaturedDialog: PropTypes.func,
comment: PropTypes.object,
asset: PropTypes.object,
diff --git a/plugins/talk-plugin-featured-comments/client/components/TabPane.js b/plugins/talk-plugin-featured-comments/client/components/TabPane.js
index 084de7d0a..9bdba6fe8 100644
--- a/plugins/talk-plugin-featured-comments/client/components/TabPane.js
+++ b/plugins/talk-plugin-featured-comments/client/components/TabPane.js
@@ -1,7 +1,6 @@
import React from 'react';
import Comment from '../containers/Comment';
import LoadMore from './LoadMore';
-import { getErrorMessages } from 'plugin-api/beta/client/utils';
class TabPane extends React.Component {
state = {
@@ -15,9 +14,8 @@ class TabPane extends React.Component {
.then(() => {
this.setState({ loadingState: 'success' });
})
- .catch(error => {
+ .catch(() => {
this.setState({ loadingState: 'error' });
- this.props.notify('error', getErrorMessages(error));
});
};
diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js
index 1f210cb95..7c8f94fb3 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/ModActionButton.js
@@ -13,8 +13,8 @@ const mapDispatchToProps = dispatch =>
);
const enhance = compose(
- withTags('featured'),
- connect(null, mapDispatchToProps)
+ connect(null, mapDispatchToProps),
+ withTags('featured')
);
export default enhance(ModActionButton);
diff --git a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
index 990a8a2e9..42c653418 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/ModTag.js
@@ -3,12 +3,10 @@ import { withTags, connect } from 'plugin-api/beta/client/hocs';
import { gql, compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { openFeaturedDialog } from '../actions';
-import { notify } from 'plugin-api/beta/client/actions/notification';
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
- notify,
openFeaturedDialog,
},
dispatch
@@ -24,8 +22,8 @@ const fragments = {
`,
};
const enhance = compose(
- withTags('featured', { fragments }),
- connect(null, mapDispatchToProps)
+ connect(null, mapDispatchToProps),
+ withTags('featured', { fragments })
);
export default enhance(ModTag);
diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
index 5bf92fb0a..297f5aedf 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
@@ -4,7 +4,6 @@ import { compose, gql } from 'react-apollo';
import TabPane from '../components/TabPane';
import { withFragments, connect } from 'plugin-api/beta/client/hocs';
import Comment from '../containers/Comment';
-import { notify } from 'plugin-api/beta/client/actions/notification';
import { viewComment } from 'coral-embed-stream/src/actions/stream';
import {
appendNewNodes,
@@ -81,7 +80,6 @@ const mapDispatchToProps = dispatch =>
bindActionCreators(
{
viewComment,
- notify,
},
dispatch
);
diff --git a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js
index 733dab756..5aa834034 100644
--- a/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js
+++ b/plugins/talk-plugin-ignore-user/client/containers/IgnoreUserConfirmation.js
@@ -10,21 +10,16 @@ import { bindActionCreators } from 'redux';
import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions';
import { notify } from 'plugin-api/beta/client/actions/notification';
import { t } from 'plugin-api/beta/client/services';
-import { getErrorMessages } from 'plugin-api/beta/client/utils';
class IgnoreUserConfirmationContainer extends React.Component {
ignoreUser = () => {
const { ignoreUser, notify, comment, closeMenu } = this.props;
- ignoreUser(comment.user.id)
- .then(() => {
- notify(
- 'success',
- t('talk-plugin-ignore-user.notify_success', comment.user.username)
- );
- })
- .catch(err => {
- notify('error', getErrorMessages(err));
- });
+ ignoreUser(comment.user.id).then(() => {
+ notify(
+ 'success',
+ t('talk-plugin-ignore-user.notify_success', comment.user.username)
+ );
+ });
closeMenu();
};
diff --git a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js
index 70cb2ce4b..1f7bed3bb 100644
--- a/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js
+++ b/plugins/talk-plugin-moderation-actions/client/containers/ApproveCommentAction.js
@@ -1,23 +1,16 @@
import React from 'react';
import { compose } from 'react-apollo';
-import { bindActionCreators } from 'redux';
-import { getErrorMessages } from 'plugin-api/beta/client/utils';
-import { notify } from 'plugin-api/beta/client/actions/notification';
import ApproveCommentAction from '../components/ApproveCommentAction';
-import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
+import { withSetCommentStatus } from 'plugin-api/beta/client/hocs';
class ApproveCommentActionContainer extends React.Component {
approveComment = async () => {
- const { setCommentStatus, comment, hideMenu, notify } = this.props;
+ const { setCommentStatus, comment, hideMenu } = this.props;
- try {
- await setCommentStatus({
- commentId: comment.id,
- status: 'ACCEPTED',
- });
- } catch (err) {
- notify('error', getErrorMessages(err));
- }
+ await setCommentStatus({
+ commentId: comment.id,
+ status: 'ACCEPTED',
+ });
hideMenu();
};
@@ -32,17 +25,6 @@ class ApproveCommentActionContainer extends React.Component {
}
}
-const mapDispatchToProps = dispatch =>
- bindActionCreators(
- {
- notify,
- },
- dispatch
- );
-
-const enhance = compose(
- connect(null, mapDispatchToProps),
- withSetCommentStatus
-);
+const enhance = compose(withSetCommentStatus);
export default enhance(ApproveCommentActionContainer);
diff --git a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js
index 6d64addca..8dd3cce7d 100644
--- a/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js
+++ b/plugins/talk-plugin-moderation-actions/client/containers/BanUserDialog.js
@@ -3,19 +3,16 @@ import PropTypes from 'prop-types';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { closeBanDialog, closeMenu } from '../actions';
-import { notify } from 'plugin-api/beta/client/actions/notification';
import {
connect,
withSetCommentStatus,
withBanUser,
} from 'plugin-api/beta/client/hocs';
-import { getErrorMessages } from 'plugin-api/beta/client/utils';
import BanUserDialog from '../components/BanUserDialog';
class BanUserDialogContainer extends React.Component {
banUser = async () => {
const {
- notify,
authorId,
commentId,
commentStatus,
@@ -25,23 +22,19 @@ class BanUserDialogContainer extends React.Component {
banUser,
} = this.props;
- try {
- await banUser({
- id: authorId,
- message: '',
+ await banUser({
+ id: authorId,
+ message: '',
+ });
+
+ closeMenu();
+ closeBanDialog();
+
+ if (commentStatus !== 'REJECTED') {
+ await setCommentStatus({
+ commentId: commentId,
+ status: 'REJECTED',
});
-
- closeMenu();
- closeBanDialog();
-
- if (commentStatus !== 'REJECTED') {
- await setCommentStatus({
- commentId: commentId,
- status: 'REJECTED',
- });
- }
- } catch (err) {
- notify('error', getErrorMessages(err));
}
};
@@ -70,7 +63,6 @@ const mapStateToProps = ({ talkPluginModerationActions: state }) => ({
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
- notify,
closeBanDialog,
closeMenu,
},
diff --git a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js
index e49fdddba..54ca0420d 100644
--- a/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js
+++ b/plugins/talk-plugin-moderation-actions/client/containers/RejectCommentAction.js
@@ -1,23 +1,16 @@
import React from 'react';
import { compose } from 'react-apollo';
-import { bindActionCreators } from 'redux';
-import { getErrorMessages } from 'plugin-api/beta/client/utils';
-import { notify } from 'plugin-api/beta/client/actions/notification';
import RejectCommentAction from '../components/RejectCommentAction';
-import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
+import { withSetCommentStatus } from 'plugin-api/beta/client/hocs';
class RejectCommentActionContainer extends React.Component {
rejectComment = async () => {
- const { setCommentStatus, comment, hideMenu, notify } = this.props;
+ const { setCommentStatus, comment, hideMenu } = this.props;
- try {
- await setCommentStatus({
- commentId: comment.id,
- status: 'REJECTED',
- });
- } catch (err) {
- notify('error', getErrorMessages(err));
- }
+ await setCommentStatus({
+ commentId: comment.id,
+ status: 'REJECTED',
+ });
hideMenu();
};
@@ -27,17 +20,6 @@ class RejectCommentActionContainer extends React.Component {
}
}
-const mapDispatchToProps = dispatch =>
- bindActionCreators(
- {
- notify,
- },
- dispatch
- );
-
-const enhance = compose(
- connect(null, mapDispatchToProps),
- withSetCommentStatus
-);
+const enhance = compose(withSetCommentStatus);
export default enhance(RejectCommentActionContainer);
diff --git a/public/javascripts/admin.js b/public/javascripts/admin.js
deleted file mode 100644
index 2c8b7c42a..000000000
--- a/public/javascripts/admin.js
+++ /dev/null
@@ -1,8 +0,0 @@
-function showError(error) {
- try {
- let err = JSON.parse(error);
- $('.error-console').text(err.message).addClass('active');
- } catch (err) {
- $('.error-console').text(error).addClass('active');
- }
-}
diff --git a/public/javascripts/auth-callback.js b/public/javascripts/auth-callback.js
deleted file mode 100644
index f91f4d743..000000000
--- a/public/javascripts/auth-callback.js
+++ /dev/null
@@ -1,4 +0,0 @@
-document.addEventListener('DOMContentLoaded', function(event) {
- localStorage.setItem('auth', document.getElementById('auth').innerText);
- setTimeout(function() { window.close(); }, 50);
-});
\ No newline at end of file
diff --git a/services/moderation/index.js b/services/moderation/index.js
new file mode 100644
index 000000000..4d87e190f
--- /dev/null
+++ b/services/moderation/index.js
@@ -0,0 +1,130 @@
+const errors = require('../../errors');
+const get = require('lodash/get');
+
+// Load in the phases to use.
+const {
+ wordlist,
+ commentLength,
+ assetClosed,
+ karma,
+ staff,
+ links,
+ premod,
+} = require('./phases');
+
+// This phase checks to see if the comment was already prescribed a status. This
+// essentially provides a hook for plugins to inject their own comments.
+const applyPreexisting = (ctx, comment) => {
+ const status = get(comment, 'status');
+
+ // If the status was already defined, don't redefine it. It's only defined
+ // when specific external conditions exist, we don't want to override that.
+ if (status) {
+ return {
+ status,
+ };
+ }
+};
+
+// Applies the defaulted status.
+const applyStatus = status => () => ({ status });
+
+/**
+ * phases is an array of moderation phases carried out in order until a status is
+ * returned.
+ */
+const phases = [
+ commentLength,
+ assetClosed,
+ wordlist,
+ staff,
+ links,
+ karma,
+ applyPreexisting,
+ premod,
+ applyStatus('NONE'),
+];
+
+/**
+ * compose will create a moderation pipeline for which is executable with the
+ * passed actions.
+ *
+ * @param {Array} phases the set of moderation phases to pass the comment and
+ * their options through.
+ */
+const compose = phases => async (ctx, comment, options) => {
+ const actions = get(comment, 'actions', []);
+
+ // Loop over all the moderation phases and see if we've resolved the status.
+ for (const phase of phases) {
+ const result = await phase(ctx, comment, options);
+ if (result) {
+ if (result.actions) {
+ actions.push(...result.actions);
+ }
+
+ // If this result contained a status, then we've finished resolving
+ // phases!
+ if (result.status) {
+ return { status: result.status, actions };
+ }
+ }
+ }
+};
+
+/**
+ * fetchOptions will generate the options used by the moderation service to
+ * determine the end status.
+ *
+ * @param {Object} ctx graph context
+ * @param {Object} comment comment object to use
+ */
+const fetchOptions = async (ctx, comment) => {
+ const {
+ connectors: { services: { Assets: AssetsService } },
+ loaders: { Settings, Assets },
+ } = ctx;
+
+ // Load the settings.
+ const settings = await Settings.load();
+
+ // Pull the asset id out of the comment.
+ const assetID = get(comment, 'asset_id', null);
+ if (assetID === null) {
+ // And leave now if this asset wasn't found.
+ throw errors.ErrNotFound;
+ }
+
+ // Load the asset.
+ const asset = await Assets.getByID.load(assetID);
+ if (!asset) {
+ // And leave now if this asset wasn't found.
+ throw errors.ErrNotFound;
+ }
+
+ // Combine the asset and the settings to get the asset settings.
+ asset.settings = await AssetsService.rectifySettings(asset, settings);
+
+ // Create the options that will be consumed by the phases.
+ return {
+ asset,
+ settings,
+ };
+};
+
+/**
+ * process the comment and return moderation details.
+ *
+ * @param {Object} ctx graphql context
+ * @param {Object} comment comment to perform the moderation phases on
+ */
+const process = async (ctx, comment) => {
+ // Fetch the options to use for the moderation phases.
+ const options = await fetchOptions(ctx, comment);
+
+ // Compose a moderation pipeline from the moderation phases and execute it on
+ // the comment.
+ return compose(phases)(ctx, comment, options);
+};
+
+module.exports.process = process;
diff --git a/services/moderation/phases/assetClosed.js b/services/moderation/phases/assetClosed.js
new file mode 100644
index 000000000..075028764
--- /dev/null
+++ b/services/moderation/phases/assetClosed.js
@@ -0,0 +1,9 @@
+const { ErrAssetCommentingClosed } = require('../../../errors');
+
+// This phase checks to see if the asset being processed is closed or not.
+module.exports = (ctx, comment, { asset }) => {
+ // Check to see if the asset has closed commenting...
+ if (asset.isClosed) {
+ throw new ErrAssetCommentingClosed(asset.closedMessage);
+ }
+};
diff --git a/services/moderation/phases/commentLength.js b/services/moderation/phases/commentLength.js
new file mode 100644
index 000000000..925115326
--- /dev/null
+++ b/services/moderation/phases/commentLength.js
@@ -0,0 +1,31 @@
+const { ErrCommentTooShort } = require('../../../errors');
+
+// This phase checks to see if the comment is long enough.
+module.exports = (
+ ctx,
+ comment,
+ { asset: { settings: { charCountEnable, charCount } } }
+) => {
+ // Check to see if the body is too short, if it is, then complain about it!
+ if (comment.body.length < 2) {
+ throw ErrCommentTooShort;
+ }
+
+ // Reject if the comment is too long
+ if (charCountEnable && comment.body.length > charCount) {
+ // Add the flag related to Trust to the comment.
+ return {
+ status: 'REJECTED',
+ actions: [
+ {
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'BODY_COUNT',
+ metadata: {
+ count: comment.body.length,
+ },
+ },
+ ],
+ };
+ }
+};
diff --git a/services/moderation/phases/index.js b/services/moderation/phases/index.js
new file mode 100644
index 000000000..a1c2e7bf5
--- /dev/null
+++ b/services/moderation/phases/index.js
@@ -0,0 +1,7 @@
+module.exports.wordlist = require('./wordlist');
+module.exports.commentLength = require('./commentLength');
+module.exports.assetClosed = require('./assetClosed');
+module.exports.karma = require('./karma');
+module.exports.staff = require('./staff');
+module.exports.links = require('./links');
+module.exports.premod = require('./premod');
diff --git a/services/moderation/phases/karma.js b/services/moderation/phases/karma.js
new file mode 100644
index 000000000..ad55b37a7
--- /dev/null
+++ b/services/moderation/phases/karma.js
@@ -0,0 +1,33 @@
+const get = require('lodash/get');
+
+// This phase checks to see if the user making the comment is allowed to do so
+// considering their reliability (Trust) status.
+module.exports = ctx => {
+ const { connectors: { services: { Karma } } } = ctx;
+ const trust = get(ctx, 'user.metadata.trust', null);
+
+ if (trust !== null) {
+ // If the user is not a reliable commenter (passed the unreliability
+ // threshold by having too many rejected comments) then we can change the
+ // status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's
+ // comments away from the public eye until a moderator can manage them. This of
+ // course can only be applied if the comment's current status is `NONE`,
+ // we don't want to interfere if the comment was rejected.
+ if (Karma.isReliable('comment', trust) === false) {
+ // Add the flag related to Trust to the comment.
+ return {
+ status: 'SYSTEM_WITHHELD',
+ actions: [
+ {
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'TRUST',
+ metadata: {
+ trust,
+ },
+ },
+ ],
+ };
+ }
+ }
+};
diff --git a/services/moderation/phases/links.js b/services/moderation/phases/links.js
new file mode 100644
index 000000000..0aed4ecd9
--- /dev/null
+++ b/services/moderation/phases/links.js
@@ -0,0 +1,26 @@
+const linkify = require('linkify-it')().tlds(require('tlds'));
+
+// This phase checks the comment if it has any links in it if the check is
+// enabled.
+module.exports = (
+ ctx,
+ comment,
+ { asset: { settings: { premodLinksEnable } } }
+) => {
+ if (premodLinksEnable && linkify.test(comment.body)) {
+ // Add the flag related to Trust to the comment.
+ return {
+ status: 'SYSTEM_WITHHELD',
+ actions: [
+ {
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'LINKS',
+ metadata: {
+ links: comment.body,
+ },
+ },
+ ],
+ };
+ }
+};
diff --git a/services/moderation/phases/premod.js b/services/moderation/phases/premod.js
new file mode 100644
index 000000000..cbd3627b1
--- /dev/null
+++ b/services/moderation/phases/premod.js
@@ -0,0 +1,15 @@
+// This phase checks to see if the settings have premod enabled, if they do,
+// the comment is premod, otherwise, it's just none.
+module.exports = (ctx, comment, { asset: { settings: { moderation } } }) => {
+ // If the settings say that we're in premod mode, then the comment is in
+ // premod status.
+ if (moderation === 'PRE') {
+ return {
+ status: 'PREMOD',
+ };
+ }
+
+ return {
+ status: 'NONE',
+ };
+};
diff --git a/services/moderation/phases/staff.js b/services/moderation/phases/staff.js
new file mode 100644
index 000000000..4b6b74946
--- /dev/null
+++ b/services/moderation/phases/staff.js
@@ -0,0 +1,10 @@
+const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../../config');
+
+// If a given user is a staff member, always approve their comment.
+module.exports = ctx => {
+ if (IGNORE_FLAGS_AGAINST_STAFF && ctx.user && ctx.user.isStaff()) {
+ return {
+ status: 'ACCEPTED',
+ };
+ }
+};
diff --git a/services/moderation/phases/wordlist.js b/services/moderation/phases/wordlist.js
new file mode 100644
index 000000000..b71e0361e
--- /dev/null
+++ b/services/moderation/phases/wordlist.js
@@ -0,0 +1,56 @@
+const { DISABLE_AUTOFLAG_SUSPECT_WORDS } = require('../../../config');
+
+// This phase checks the comment against the wordlist.
+module.exports = async (ctx, comment, { settings }) => {
+ const { connectors: { services: { Wordlist } } } = ctx;
+
+ // Create a new instance of the Wordlist.
+ const wl = new Wordlist();
+
+ // Load the wordlist.
+ wl.upsert(settings.wordlist);
+
+ // Scan the comment body for wordlist violations.
+ const { banned = null, suspect = null } = wl.scan('body', comment.body);
+
+ // Decide the status based on whether or not the current asset/settings
+ // has pre-mod enabled or not. If the comment was rejected based on the
+ // wordlist, then reject it, otherwise if the moderation setting is
+ // premod, set it to `premod`.
+ if (banned) {
+ // Add the flag related to Trust to the comment.
+ return {
+ status: 'REJECTED',
+ actions: [
+ {
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'BANNED_WORD',
+ metadata: {},
+ },
+ ],
+ };
+ }
+
+ // If the comment has a suspect word or a link, we need to add a
+ // flag to it to indicate that it needs to be looked at.
+ // Otherwise just return the new comment.
+
+ // If the wordlist has matched the suspect word filter and we haven't disabled
+ // auto-flagging suspect words, then we should flag the comment!
+ if (suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
+ // TODO: this is kind of fragile, we should refactor this to resolve
+ // all these const's that we're using like 'COMMENTS', 'FLAG' to be
+ // defined in a checkable schema.
+ return {
+ actions: [
+ {
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'SUSPECT_WORD',
+ metadata: {},
+ },
+ ],
+ };
+ }
+};
diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js
index 8a5a98aa1..720297e12 100644
--- a/test/server/graph/mutations/createComment.js
+++ b/test/server/graph/mutations/createComment.js
@@ -49,6 +49,9 @@ describe('graph.mutations.createComment', () => {
return graphql(schema, query, {}, context).then(
({ data, errors }) => {
+ if (errors) {
+ console.error(errors);
+ }
expect(errors).to.be.undefined;
if (error) {
expect(data.createComment).to.have.property('comment').null;
@@ -98,7 +101,9 @@ describe('graph.mutations.createComment', () => {
async () => {
const context = new Context({ user });
const { data, errors } = await graphql(schema, query, {}, context);
-
+ if (errors) {
+ console.error(errors);
+ }
expect(errors).to.be.undefined;
if (error) {
expect(data.createComment).to.have.property('comment').null;
diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs
index 0bc54990b..f8f6c62f2 100644
--- a/views/admin/confirm-email.ejs
+++ b/views/admin/confirm-email.ejs
@@ -20,9 +20,17 @@
-
-
-
+