Add toggleable profanity filter to text channels (disabled by default).
This commit is contained in:
parent
85144561c4
commit
e467aabddf
24
backend/package-lock.json
generated
24
backend/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@acrd/ion": "github:acrdapp/ion",
|
||||
"@acrd/types": "file:../frontend/src/types",
|
||||
"bad-words": "^3.0.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"chai-things": "^0.2.0",
|
||||
"colors": "^1.4.0",
|
||||
@ -44,6 +45,7 @@
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bad-words": "^3.0.1",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/chai-spies": "^1.0.3",
|
||||
@ -1454,6 +1456,12 @@
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/bad-words": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/bad-words/-/bad-words-3.0.1.tgz",
|
||||
"integrity": "sha512-7la3ZDJG1tlRqySO+pnXycZpacaMEw/iLEm8kc4l+I+jN8KjBfoQVwO6jm98xzXVLrxV8vDrB5TaMoop8sKclQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||
@ -2155,6 +2163,22 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bad-words": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bad-words/-/bad-words-3.0.4.tgz",
|
||||
"integrity": "sha512-v/Q9uRPH4+yzDVLL4vR1+S9KoFgOEUl5s4axd6NIAq8SV2mradgi4E8lma/Y0cw1ltVdvyegCQQKffCPRCp8fg==",
|
||||
"dependencies": {
|
||||
"badwords-list": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/badwords-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/badwords-list/-/badwords-list-1.0.0.tgz",
|
||||
"integrity": "sha512-oWhaSG67e+HQj3OGHQt2ucP+vAPm1wTbdp2aDHeuh4xlGXBdWwzZ//pfu6swf5gZ8iX0b7JgmSo8BhgybbqszA=="
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@acrd/ion": "github:acrdapp/ion",
|
||||
"@acrd/types": "file:../frontend/src/types",
|
||||
"bad-words": "^3.0.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"chai-things": "^0.2.0",
|
||||
"colors": "^1.4.0",
|
||||
@ -49,6 +50,7 @@
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bad-words": "^3.0.1",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/chai-spies": "^1.0.3",
|
||||
|
@ -13,7 +13,7 @@ export default class Messages extends DBWrapper<string, MessageDocument> {
|
||||
}
|
||||
|
||||
public async create(authorId: string, channelId: string, { attachmentURLs, content }: Partial<Entity.Message>) {
|
||||
// TODO: TESTME
|
||||
// TODO: TESTME
|
||||
if (!content && !attachmentURLs?.length)
|
||||
throw new TypeError('Empty messages are not valid');
|
||||
|
||||
|
@ -14,6 +14,7 @@ export interface VoiceChannelDocument extends Document, ChannelTypes.Voice {
|
||||
id: string;
|
||||
createdAt: never;
|
||||
memberIds: string[];
|
||||
filterProfanity: never;
|
||||
}
|
||||
export type ChannelDocument = TextChannelDocument | VoiceChannelDocument;
|
||||
|
||||
@ -36,6 +37,7 @@ export const Channel = model<ChannelDocument>('channel', new Schema({
|
||||
maxlength: [32, 'Name too long'],
|
||||
validate: [validators.textChannelName, 'Invalid name'],
|
||||
},
|
||||
filterProfanity: { type: Boolean },
|
||||
firstMessageId: {
|
||||
type: String,
|
||||
validate: [validators.optionalSnowflake, 'Invalid Snowflake ID'],
|
||||
|
@ -7,7 +7,7 @@ import { Entity, WS } from '@acrd/types';
|
||||
export default class implements WSEvent<'CHANNEL_UPDATE'> {
|
||||
public on = 'CHANNEL_UPDATE' as const;
|
||||
|
||||
public async invoke(ws: WebSocket, client: Socket, { position, name, summary, overrides, channelId }: WS.Params.ChannelUpdate) {
|
||||
public async invoke(ws: WebSocket, client: Socket, { position, name, summary, filterProfanity, overrides, channelId }: WS.Params.ChannelUpdate) {
|
||||
const channel = await deps.channels.get(channelId);
|
||||
await deps.wsGuard.validateCan(client, channel.guildId, 'MANAGE_CHANNELS');
|
||||
|
||||
@ -20,6 +20,9 @@ export default class implements WSEvent<'CHANNEL_UPDATE'> {
|
||||
await this.raiseHigherChannels(position, channel);
|
||||
}
|
||||
|
||||
if (filterProfanity != undefined)
|
||||
partialChannel.filterProfanity = filterProfanity;
|
||||
|
||||
Object.assign(channel, partialChannel);
|
||||
await channel.save();
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { WSEvent, } from './ws-event';
|
||||
import { Channel } from '../../data/models/channel';
|
||||
import striptags from 'striptags';
|
||||
import { WS } from '@acrd/types';
|
||||
import ProfanityFilter from 'bad-words';
|
||||
|
||||
export default class implements WSEvent<'MESSAGE_CREATE'> {
|
||||
on = 'MESSAGE_CREATE' as const;
|
||||
@ -11,17 +12,20 @@ export default class implements WSEvent<'MESSAGE_CREATE'> {
|
||||
public async invoke(ws: WebSocket, client: Socket, { attachmentURLs, channelId, content, embed }: WS.Params.MessageCreate) {
|
||||
const authorId = ws.sessions.userId(client);
|
||||
|
||||
const [_, message, author] = await Promise.all([
|
||||
const [_, channel, author] = await Promise.all([
|
||||
deps.wsGuard.validateCanInChannel(client, channelId, 'SEND_MESSAGES'),
|
||||
deps.messages.create(authorId, channelId, {
|
||||
attachmentURLs,
|
||||
content: (content) ? striptags(content) : '',
|
||||
embed,
|
||||
}),
|
||||
deps.channels.getText(channelId),
|
||||
deps.users.getSelf(authorId),
|
||||
]);
|
||||
|
||||
await Channel.updateOne({ _id: channelId }, { lastMessageId: message.id });
|
||||
var message = await deps.messages.create(authorId, channelId, {
|
||||
attachmentURLs,
|
||||
content: this.filterContent(content, channel.filterProfanity),
|
||||
embed,
|
||||
});
|
||||
|
||||
channel.lastMessageId = message.id;
|
||||
await channel.save();
|
||||
|
||||
author.lastReadMessageIds ??= {};
|
||||
author.lastReadMessageIds[channelId] = message.id;
|
||||
@ -33,4 +37,13 @@ export default class implements WSEvent<'MESSAGE_CREATE'> {
|
||||
send: { message },
|
||||
}];
|
||||
}
|
||||
|
||||
private filterContent(content: string | undefined, filterProfanity: boolean) {
|
||||
const badWords = new ProfanityFilter({ placeHolder: '?' });
|
||||
if (content && filterProfanity)
|
||||
return striptags(badWords.clean(content));
|
||||
else if (content)
|
||||
return striptags(content);
|
||||
return '';
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ const MessageHeader: FunctionComponent<MessageHeaderProps> = ({ author, message,
|
||||
<div>
|
||||
<ContextMenuTrigger id={author.id}>
|
||||
<span
|
||||
style={{ color: highestRole.color }}
|
||||
style={{ color: highestRole?.color }}
|
||||
className="text-base heading hover:underline cursor-pointer mr-2">{author.username}</span>
|
||||
</ContextMenuTrigger>
|
||||
<span className="text-xs">
|
||||
|
@ -2,13 +2,56 @@ input {
|
||||
border: 1px solid var(--bg-secondary-alt);
|
||||
caret-color: var(--heading);
|
||||
}
|
||||
|
||||
input:hover {
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: 1px solid var(--link);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: var(--muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* native toggle */
|
||||
input[type=checkbox] {
|
||||
transform: scale(.75);
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--danger);
|
||||
outline: none;
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 0 5px rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
input[type="checkbox"]::before {
|
||||
content: "";
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::before {
|
||||
transform: translateX(100%);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background: var(--success);
|
||||
}
|
@ -9,17 +9,17 @@ import ReactTooltip from 'react-tooltip';
|
||||
|
||||
export interface InputProps {
|
||||
name: string;
|
||||
register?: UseFormRegister<FieldValues>;
|
||||
options?: any;
|
||||
autoFocus?: boolean;
|
||||
options?: any;
|
||||
label?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
register?: UseFormRegister<FieldValues>;
|
||||
setFocusedInputId?: (val: any) => any;
|
||||
}
|
||||
|
||||
|
||||
const Input: React.FunctionComponent<InputProps & React.AllHTMLAttributes<HTMLInputElement>> = (props) => {
|
||||
const { label, name, register, options, type, autoFocus, className, disabled, tooltip } = props;
|
||||
const id = name + 'Input';
|
||||
@ -59,5 +59,5 @@ const Input: React.FunctionComponent<InputProps & React.AllHTMLAttributes<HTMLIn
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default Input;
|
@ -6,11 +6,10 @@ import Input from '../../inputs/input';
|
||||
import NormalButton from '../../utils/buttons/normal-button';
|
||||
import Category from '../../utils/category';
|
||||
import SaveChanges from '../../utils/save-changes';
|
||||
|
||||
|
||||
const ChannelSettingsOverview: React.FunctionComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
const channel = useSelector((s: Store.AppState) => s.ui.activeChannel)!;
|
||||
const guild = useSelector((s: Store.AppState) => s.ui.activeGuild)!;
|
||||
const { register, handleSubmit, setValue } = useForm();
|
||||
|
||||
const onSave = (e) => {
|
||||
@ -21,7 +20,7 @@ const ChannelSettingsOverview: React.FunctionComponent = () => {
|
||||
const confirmation = window.confirm('Are you sure you want to delete this guild?');
|
||||
confirmation && dispatch(deleteChannel(channel.id));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
onChange={() => dispatch(openSaveChanges(true))}
|
||||
@ -29,7 +28,7 @@ const ChannelSettingsOverview: React.FunctionComponent = () => {
|
||||
<header>
|
||||
<h1 className="text-xl font-bold inline">Channel Overview</h1>
|
||||
</header>
|
||||
|
||||
|
||||
<section className="w-1/3">
|
||||
<Input
|
||||
label="Name"
|
||||
@ -43,6 +42,14 @@ const ChannelSettingsOverview: React.FunctionComponent = () => {
|
||||
register={register}
|
||||
options={{ value: channel.summary }}
|
||||
className="pt-5" />
|
||||
<Input
|
||||
tooltip='Mask explicit words in messages.'
|
||||
label="Filter Profanity"
|
||||
name="filterProfanity"
|
||||
type="checkbox"
|
||||
register={register}
|
||||
options={{ value: channel.filterProfanity }}
|
||||
className="pt-5" />
|
||||
</section>
|
||||
|
||||
<Category
|
||||
@ -60,8 +67,8 @@ const ChannelSettingsOverview: React.FunctionComponent = () => {
|
||||
setValue={setValue}
|
||||
onSave={onSave}
|
||||
obj={channel} />
|
||||
</form>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ChannelSettingsOverview;
|
@ -61,28 +61,28 @@ const UserSettingsOverview: React.FunctionComponent = () => {
|
||||
</form>
|
||||
|
||||
<Category
|
||||
className="py-2 mt-5"
|
||||
title="Advanced Settings" />
|
||||
className="py-2 mt-5"
|
||||
title="Advanced Settings" />
|
||||
|
||||
<section>
|
||||
<div className="w-1/2 pb-5">
|
||||
<label htmlFor="devMode">Dev Mode</label>
|
||||
<Toggle
|
||||
onChange={(e) => e.stopPropagation()}
|
||||
onClick={() => dispatch(toggleDevMode())}
|
||||
checked={devMode}
|
||||
className="float-right"
|
||||
id="devMode" />
|
||||
</div>
|
||||
<div className="w-1/2 pb-5">
|
||||
<label htmlFor="devMode">Dev Mode</label>
|
||||
<Toggle
|
||||
onChange={(e) => e.stopPropagation()}
|
||||
onClick={() => dispatch(toggleDevMode())}
|
||||
checked={devMode}
|
||||
className="float-right"
|
||||
id="devMode" />
|
||||
</div>
|
||||
|
||||
<NormalButton
|
||||
id="deleteUserButton"
|
||||
role="button"
|
||||
onClick={handleSubmit(onDelete)}
|
||||
className="bg-danger">Delete</NormalButton>
|
||||
<NormalButton
|
||||
id="deleteUserButton"
|
||||
role="button"
|
||||
onClick={handleSubmit(onDelete)}
|
||||
className="bg-danger">Delete</NormalButton>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UserSettingsOverview;
|
@ -10,11 +10,10 @@ export interface SaveChangesProps {
|
||||
onSave: (e) => any;
|
||||
onOpen?: () => any;
|
||||
onReset?: (e) => any;
|
||||
/** @deprecated */
|
||||
setValue?: UseFormSetValue<FieldValues>;
|
||||
obj: object;
|
||||
}
|
||||
|
||||
|
||||
const SaveChanges: React.FunctionComponent<SaveChangesProps> = (props) => {
|
||||
const { closeSnackbar, enqueueSnackbar } = useSnackbar();
|
||||
const dispatch = useDispatch();
|
||||
@ -63,5 +62,5 @@ const SaveChanges: React.FunctionComponent<SaveChangesProps> = (props) => {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export default SaveChanges;
|
@ -13,7 +13,6 @@ const slice = createSlice({
|
||||
stoppedEditingMessage: (state) => {
|
||||
delete state.editingMessageId;
|
||||
},
|
||||
// only 1 invite is created -> to save data, and stop spam
|
||||
focusedResource: (state, { payload }) => {
|
||||
state.activeResource = payload;
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user