mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 23:26:19 +08:00
Merge branch 'master' into events
This commit is contained in:
+29
-24
@@ -1,34 +1,39 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
**/*.html
|
||||
node_modules
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
public
|
||||
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-notifications
|
||||
!plugins/talk-plugin-notifications-reply
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-viewing-options
|
||||
|
||||
public
|
||||
node_modules
|
||||
node_modules
|
||||
@@ -23,6 +23,7 @@ browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
@@ -152,27 +153,27 @@ class UserDetail extends React.Component {
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
Remove Suspension
|
||||
{t('user_detail.remove_suspension')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showSuspenUserDialog}
|
||||
>
|
||||
Suspend User
|
||||
{t('user_detail.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{banned ? (
|
||||
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
|
||||
Remove Ban
|
||||
{t('user_detail.remove_ban')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showBanUserDialog}
|
||||
>
|
||||
Ban User
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
@@ -190,14 +191,18 @@ class UserDetail extends React.Component {
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>Email:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
@@ -210,17 +215,23 @@ class UserDetail extends React.Component {
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Total Comments</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.total_comments')}
|
||||
</span>
|
||||
<span className={styles.statResult}>{totalComments}</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reject Rate</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reject_rate')}
|
||||
</span>
|
||||
<span className={styles.statResult}>
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reports</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
@@ -259,13 +270,13 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-all-tab'
|
||||
)}
|
||||
>
|
||||
All
|
||||
{t('user_detail.all')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'rejected'}
|
||||
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
|
||||
>
|
||||
Rejected
|
||||
{t('user_detail.rejected')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'history'}
|
||||
@@ -275,7 +286,7 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
Account History
|
||||
{t('user_detail.account_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class LayoutContainer extends React.Component {
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<Layout handleLogout={logout} {...this.props}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
|
||||
@@ -19,12 +19,12 @@ class ViewOptions extends React.Component {
|
||||
'talk-admin-moderation-view-options-headline'
|
||||
)}
|
||||
>
|
||||
View Options
|
||||
{t('admin_sidebar.view_options')}
|
||||
</h2>
|
||||
<div className={styles.viewOptionsContent}>
|
||||
<ul className={styles.viewOptionsList}>
|
||||
<li className={styles.viewOptionsItem}>
|
||||
Sort Comments
|
||||
{t('admin_sidebar.sort_comments')}
|
||||
<Dropdown
|
||||
containerClassName={styles.dropdownContainer}
|
||||
toggleClassName={styles.dropdownToggle}
|
||||
|
||||
@@ -407,6 +407,7 @@ export const subscriptionFields = `
|
||||
type
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
|
||||
@@ -103,3 +103,18 @@ Source: [plugins/talk-plugin-subscriber](https://github.com/coralproject/talk/tr
|
||||
Enables a `Subscriber` badge to be added to comments where the author has the
|
||||
`SUBSCRIBER` tag. This must match with a custom auth integration that adds the
|
||||
tag to the users that are subscribed to the service.
|
||||
|
||||
## talk-plugin-akismet
|
||||
|
||||
Source: [plugins/talk-plugin-akismet](https://github.com/coralproject/talk/tree/master/plugins/talk-plugin-akismet){:target="_blank"}
|
||||
|
||||
Enables spam detection from [Akismet](https://akismet.com/). Comments will be passed to the Akismet API for spam detection. If a comment
|
||||
is determined to be spam, it will prompt the user, indicating that the comment might be considered spam. If the user continues after this
|
||||
point with the still spam-like comment, the comment will be reported as containing spam, and sent for moderator approval.
|
||||
|
||||
**Note: [Akismet](https://akismet.com/) is a premium service, charges may apply.**
|
||||
|
||||
Configuration:
|
||||
|
||||
- `TALK_AKISMET_API_KEY` (**required**) - The Akismet API key located on your account page
|
||||
- `TALK_AKISMET_SITE` (**required**) - The URL for your site that the comment are appearing on (not the root url, the url for the articles)
|
||||
+1
-1
@@ -9,7 +9,7 @@ module.exports = {
|
||||
// Schema is created already, so just include it.
|
||||
schema,
|
||||
|
||||
// Load in the new context here, this'll create the loaders + mutators for
|
||||
// Load in the new context here, this will create the loaders + mutators for
|
||||
// the lifespan of this request.
|
||||
context: new Context(req),
|
||||
|
||||
|
||||
+1
-1
@@ -306,7 +306,7 @@ da:
|
||||
stream:
|
||||
all_comments: "Alle kommentarer"
|
||||
temporarily_suspended: "I overensstemmelse med {0}'s retningslinjer for fællesskabet er din konto midlertidigt suspenderet. Venligst tilslut dig samtalen {1}."
|
||||
comment_not_found: "Kommentar blev ikke fundet"
|
||||
comment_not_found: "Denne kommentar er blevet fjernet eller findes ikke."
|
||||
step_1_header: "Rapportér et problem"
|
||||
step_2_header: "Hjælp os med at forstå"
|
||||
step_3_header: "Tak for din indsats"
|
||||
|
||||
+17
-1
@@ -366,7 +366,7 @@ en:
|
||||
stream:
|
||||
all_comments: "All Comments"
|
||||
temporarily_suspended: "In accordance with {0}'s community guidelines, your account has been temporarily suspended. Please rejoin the conversation {1}."
|
||||
comment_not_found: "Comment was not found"
|
||||
comment_not_found: "This comment has been removed or does not exist."
|
||||
no_comments: "There are no comments yet, why don’t you write one?"
|
||||
no_comments_and_closed: "There were no comments on this article."
|
||||
step_1_header: "Report an issue"
|
||||
@@ -418,6 +418,19 @@ en:
|
||||
bio_flags: "flags for this bio"
|
||||
user_bio: "User Bio"
|
||||
username_flags: "flags for this username"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
user_impersonating: "This user is impersonating"
|
||||
user_no_comment: "You've never left a comment. Join the conversation!"
|
||||
username_offensive: "This username is offensive"
|
||||
@@ -444,3 +457,6 @@ en:
|
||||
description: "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now."
|
||||
launch: "Launch Talk"
|
||||
close: "Close this Installer"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
|
||||
+1
-1
@@ -357,7 +357,7 @@ es:
|
||||
stream:
|
||||
all_comments: "Todos los comentarios"
|
||||
temporarily_suspended: "De acuerdo con la guía de la comunidad de {0}, su cuenta ha sido temporalmente suspendida. Por favor unirse a la conversación {1}."
|
||||
comment_not_found: "Comentario no encontrado"
|
||||
comment_not_found: "Este comentario ha sido eliminado o no existe."
|
||||
no_comments: "Todavía no hay comentarios. ¿Por qué no escribes uno?"
|
||||
no_comments_and_closed: "No hubo comentarios en este artículo."
|
||||
streams:
|
||||
|
||||
@@ -259,6 +259,7 @@ fr:
|
||||
stream:
|
||||
all_comments: "Tous les commentaires"
|
||||
temporarily_suspended: "Conformément à la charte d'utilisation des commentaires de {0}, votre compte a été temporairement suspendu. Merci de revenir dans la conversation {1}."
|
||||
comment_not_found: "Ce commentaire a été supprimé ou n'existe pas."
|
||||
step_1_header: "Signaler un problème"
|
||||
step_2_header: "Aidez-nous à comprendre"
|
||||
step_3_header: "Merci pour votre participation"
|
||||
|
||||
+1
-1
@@ -358,7 +358,7 @@ nl_NL:
|
||||
stream:
|
||||
all_comments: "Alle reacties"
|
||||
temporarily_suspended: "In overeenstemming met de community richtlijnen van {0} is je account tijdelijk geschorst. Keer terug naar de conversaties {1}."
|
||||
comment_not_found: "Reactie niet gevonden"
|
||||
comment_not_found: "Deze opmerking is verwijderd of bestaat niet."
|
||||
no_comments: "Er zijn nog geen reacties. Schrijf er zelf eentje!"
|
||||
no_comments_and_closed: "Er waren geen reacties op dit artikel."
|
||||
step_1_header: "Rapporteer een issue"
|
||||
|
||||
@@ -309,6 +309,7 @@ pt_BR:
|
||||
stream:
|
||||
all_comments: "Todos os comentários"
|
||||
temporarily_suspended: "De acordo com as diretrizes da comunidade de {0}, sua conta foi temporariamente suspensa. Por favor, volte para a conversa {1}."
|
||||
comment_not_found: "Este comentário foi removido ou não existe."
|
||||
step_1_header: "Relatar um problema"
|
||||
step_2_header: "Ajude-nos a entender"
|
||||
step_3_header: "Obrigdo por sua contribuição"
|
||||
|
||||
+1
-1
@@ -323,7 +323,7 @@ zh_CN:
|
||||
stream:
|
||||
all_comments: "所有评论"
|
||||
temporarily_suspended: "根据 {0} 的社群指引方针,您的帐号已被暂时停用。请稍后加入对话 {1}。"
|
||||
comment_not_found: "未找到评论"
|
||||
comment_not_found: "此评论已被删除或不存在。"
|
||||
no_comments: "还没有评论,不如你写个?"
|
||||
no_comments_and_closed: "该文章没有评论。"
|
||||
step_1_header: "报告问题"
|
||||
|
||||
+1
-1
@@ -323,7 +323,7 @@ zh_TW:
|
||||
stream:
|
||||
all_comments: "全部評論"
|
||||
temporarily_suspended: "根據{0}的社區規定,您的帳戶已被暫時停用。請重新加入對話{1}。"
|
||||
comment_not_found: "未發現評論"
|
||||
comment_not_found: "此評論已被刪除或不存在。"
|
||||
no_comments: "暫時還沒有評論,何不寫條評論呢?"
|
||||
no_comments_and_closed: "這篇文章沒有評論。"
|
||||
step_1_header: "舉報問題"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
/**
|
||||
* CheckSpamHook adds hooks to the `commentBox`
|
||||
* that handles checking a comment for spam.
|
||||
*/
|
||||
export default class CheckSpamHook extends React.Component {
|
||||
// checked signifies if we already sent a request with the `checkSpam` set to true.
|
||||
checked = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.spamPreHook = this.props.registerHook('preSubmit', input => {
|
||||
// If we haven't check the spam yet, make sure to include `checkSpam=true` in the mutation.
|
||||
// Otherwise post comment without checking the spam.
|
||||
if (!this.checked) {
|
||||
input.checkSpam = true;
|
||||
this.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.spamPostHook = this.props.registerHook('postSubmit', result => {
|
||||
const actions = result.createComment.actions;
|
||||
if (
|
||||
actions &&
|
||||
actions.some(
|
||||
({ __typename, reason }) =>
|
||||
__typename === 'FlagAction' && reason === 'SPAM_COMMENT'
|
||||
)
|
||||
) {
|
||||
this.props.notify('error', t('talk-plugin-akismet.still_spam'));
|
||||
}
|
||||
|
||||
// Reset `checked` after comment was successfully posted.
|
||||
this.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unregisterHook(this.spamPreHook);
|
||||
this.props.unregisterHook(this.spamPostHook);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
CheckSpamHook.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
registerHook: PropTypes.func.isRequired,
|
||||
unregisterHook: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CommentDetail } from 'plugin-api/beta/client/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const SpamLabel = () => (
|
||||
<CommentDetail
|
||||
icon={'add_box'}
|
||||
header={t('talk-plugin-akismet.spam_comment')}
|
||||
info={t('talk-plugin-akismet.detected')}
|
||||
/>
|
||||
);
|
||||
|
||||
SpamLabel.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
actions: PropTypes.array,
|
||||
spam: PropTypes.spam,
|
||||
}),
|
||||
};
|
||||
|
||||
export default SpamLabel;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { FlagLabel } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const SpamLabel = () => (
|
||||
<FlagLabel iconName="add_box">{t('talk-plugin-akismet.spam')}</FlagLabel>
|
||||
);
|
||||
|
||||
export default SpamLabel;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'plugin-api/beta/client/hocs';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import CheckSpamHook from '../components/CheckSpamHook';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CheckSpamHook);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { compose } from 'react-apollo';
|
||||
import { excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import SpamDetail from './SpamDetail';
|
||||
import { isSpam } from '../utils';
|
||||
|
||||
const enhance = compose(
|
||||
excludeIf(
|
||||
({ comment: { spam, actions } }) => spam === null || isSpam(actions)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhance(SpamDetail);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { compose } from 'react-apollo';
|
||||
import { excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import SpamDetail from './SpamDetail';
|
||||
import { isSpam } from '../utils';
|
||||
|
||||
const enhance = compose(
|
||||
excludeIf(
|
||||
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhance(SpamDetail);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import SpamDetail from '../components/SpamDetail';
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment TalkSpamComments_SpamDetail_Comment on Comment {
|
||||
spam
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(SpamDetail);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import SpamLabel from '../components/SpamLabel';
|
||||
import { isSpam } from '../utils';
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment TalkSpamComments_SpamLabel_Comment on Comment {
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
excludeIf(({ comment: { actions } }) => !isSpam(actions))
|
||||
);
|
||||
|
||||
export default enhance(SpamLabel);
|
||||
@@ -0,0 +1,15 @@
|
||||
import translations from './translations.yml';
|
||||
import CheckSpamHook from './containers/CheckSpamHook';
|
||||
import SpamLabel from './containers/SpamLabel';
|
||||
import SpamCommentDetail from './containers/SpamCommentDetail';
|
||||
import SpamCommentFlagDetail from './containers/SpamCommentFlagDetail';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
commentInputDetailArea: [CheckSpamHook],
|
||||
adminCommentLabels: [SpamLabel],
|
||||
adminCommentMoreDetails: [SpamCommentDetail],
|
||||
adminCommentMoreFlagDetails: [SpamCommentFlagDetail],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
en:
|
||||
error:
|
||||
COMMENT_IS_SPAM: |
|
||||
The language in this comment looks like spam. You can
|
||||
edit the comment or submit it anyway for moderator review.
|
||||
talk-plugin-akismet:
|
||||
spam: "Spam"
|
||||
spam_comment: "Spam"
|
||||
detected: "Detected by Akismet"
|
||||
still_spam: |
|
||||
Thank you. Our moderation team will review your comment shortly.
|
||||
flags:
|
||||
reasons:
|
||||
comment:
|
||||
spam_comment: "Detected Spam"
|
||||
@@ -0,0 +1,6 @@
|
||||
export function isSpam(actions) {
|
||||
return actions.some(
|
||||
action =>
|
||||
action.__typename === 'FlagAction' && action.reason === 'SPAM_COMMENT'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const config = {
|
||||
KEY: process.env.TALK_AKISMET_API_KEY,
|
||||
SITE: process.env.TALK_AKISMET_SITE,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV !== 'test' && (!config.KEY || !config.SITE)) {
|
||||
throw new Error(
|
||||
'Please set the TALK_AKISMET_API_KEY and TALK_AKISMET_SITE environment variable to use talk-plugin-akismet-comments. Visit https://akismet.com/ to get started.'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,13 @@
|
||||
const { APIError } = require('errors');
|
||||
|
||||
// ErrSpam is sent during a `CreateComment` mutation where
|
||||
// `input.checkSpam` is set to true and the comment contains
|
||||
// detected spam as determined by the akismet service.
|
||||
const ErrSpam = new APIError('Comment is spam', {
|
||||
status: 400,
|
||||
translation_key: 'COMMENT_IS_SPAM',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ErrSpam,
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
const debug = require('debug')('talk:plugin:akismet');
|
||||
const { ErrSpam } = require('./errors');
|
||||
const akismet = require('akismet-api');
|
||||
const { get, merge } = require('lodash');
|
||||
const { KEY, SITE } = require('./config');
|
||||
const client = akismet.client({
|
||||
key: KEY,
|
||||
blog: SITE,
|
||||
});
|
||||
|
||||
let enabled = true;
|
||||
|
||||
// TODO: when using a developer key, this is possible, the plus plan does not
|
||||
// allow us to check the key.
|
||||
// let enabled = false;
|
||||
// client.verifyKey((err, valid) => {
|
||||
// if (err) {
|
||||
// throw err;
|
||||
// }
|
||||
|
||||
// if (valid) {
|
||||
// enabled = true;
|
||||
// } else {
|
||||
// throw new Error('Akismet key is invalid');
|
||||
// }
|
||||
// });
|
||||
|
||||
module.exports = {
|
||||
typeDefs: `
|
||||
input CreateCommentInput {
|
||||
|
||||
# If true, the mutation will fail when the
|
||||
# body contains detected spam.
|
||||
checkSpam: Boolean
|
||||
}
|
||||
|
||||
type Comment {
|
||||
spam: Boolean
|
||||
}
|
||||
`,
|
||||
hooks: {
|
||||
RootMutation: {
|
||||
createComment: {
|
||||
async pre(_, { input }, { loaders, parent: req }) {
|
||||
// If the key validation failed, then we can't run with the client.
|
||||
if (!enabled) {
|
||||
debug('not enabled, passing');
|
||||
return;
|
||||
}
|
||||
|
||||
let spam = false;
|
||||
try {
|
||||
const user_ip = get(req, 'ip', false);
|
||||
if (!user_ip) {
|
||||
debug('no ip on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get some headers from the request.
|
||||
const user_agent = req.get('User-Agent');
|
||||
if (!user_agent || user_agent.length === 0) {
|
||||
debug('no user agent on request');
|
||||
return;
|
||||
}
|
||||
|
||||
const referrer = req.get('Referrer');
|
||||
if (!referrer || referrer.length === 0) {
|
||||
debug('no referrer on request');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Asset that the comment is being made against.
|
||||
const asset = await loaders.Assets.getByID.load(input.asset_id);
|
||||
if (!asset) {
|
||||
debug('asset not found for new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send off the comment to Akismet to check to see what they say.
|
||||
spam = await client.checkSpam({
|
||||
user_ip,
|
||||
user_agent,
|
||||
referrer,
|
||||
permalink: asset.url,
|
||||
comment_type: 'comment',
|
||||
comment_content: input.body,
|
||||
is_test: true,
|
||||
});
|
||||
|
||||
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
|
||||
} catch (err) {
|
||||
console.trace(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach scores to metadata.
|
||||
input.metadata = merge({}, input.metadata || {}, {
|
||||
akismet: spam,
|
||||
});
|
||||
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw ErrSpam;
|
||||
}
|
||||
|
||||
// Attach reason information for the flag being added.
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
input.actions =
|
||||
input.actions && input.actions.length >= 0 ? input.actions : [];
|
||||
input.actions.push({
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SPAM_COMMENT',
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolvers: {
|
||||
Comment: {
|
||||
spam: comment => get(comment, 'metadata.akismet', null),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@coralproject/talk-plugin-akismet",
|
||||
"pluginName": "talk-plugin-akismet",
|
||||
"version": "0.0.1",
|
||||
"description": "Provides support for preventing spam with the Akismet API",
|
||||
"main": "index.js",
|
||||
"author": "The Coral Project Team <coral@mozillafoundation.org>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"akismet-api": "^4.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
akismet-api@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/akismet-api/-/akismet-api-4.0.1.tgz#1c771442f09316847132aa16171bb4fb708b6519"
|
||||
dependencies:
|
||||
bluebird "^3.1.1"
|
||||
superagent "^3.8.0"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
|
||||
bluebird@^3.1.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
combined-stream@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
component-emitter@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
||||
|
||||
cookiejar@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
|
||||
extend@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
|
||||
form-data@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.5"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formidable@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
|
||||
|
||||
inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
||||
methods@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
|
||||
mime-db@~1.30.0:
|
||||
version "1.30.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
|
||||
dependencies:
|
||||
mime-db "~1.30.0"
|
||||
|
||||
mime@^1.4.1:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
process-nextick-args@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
|
||||
|
||||
qs@^6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
readable-stream@^2.0.5:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~1.0.6"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.0.3"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
string_decoder@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
superagent@^3.8.0:
|
||||
version "3.8.2"
|
||||
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403"
|
||||
dependencies:
|
||||
component-emitter "^1.2.0"
|
||||
cookiejar "^2.1.0"
|
||||
debug "^3.1.0"
|
||||
extend "^3.0.0"
|
||||
form-data "^2.3.1"
|
||||
formidable "^1.1.1"
|
||||
methods "^1.1.1"
|
||||
mime "^1.4.1"
|
||||
qs "^6.5.1"
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
Reference in New Issue
Block a user