summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/migration/1644010796173-convert-hard-mutes.js64
-rw-r--r--packages/backend/src/misc/check-word-mute.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts21
-rw-r--r--packages/client/src/pages/settings/word-mute.vue55
-rw-r--r--packages/client/src/scripts/check-word-mute.ts31
5 files changed, 171 insertions, 31 deletions
diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js
new file mode 100644
index 0000000000..f06da65670
--- /dev/null
+++ b/packages/backend/migration/1644010796173-convert-hard-mutes.js
@@ -0,0 +1,64 @@
+const RE2 = require('re2');
+const { MigrationInterface, QueryRunner } = require("typeorm");
+
+module.exports = class convertHardMutes1644010796173 {
+ name = 'convertHardMutes1644010796173'
+
+ async up(queryRunner) {
+ let entries = await queryRunner.query(`SELECT "userId", "mutedWords" FROM "user_profile"`);
+ for(let i = 0; i < entries.length; i++) {
+ let words = entries[i].mutedWords
+ .map(line => {
+ const regexp = line.join(" ").match(/^\/(.+)\/(.*)$/);
+ if (regexp) {
+ // convert regexp's
+ try {
+ new RE2(regexp[1], regexp[2]);
+ return `/${regexp[1]}/${regexp[2]}`;
+ } catch (err) {
+ // invalid regex, ignore it
+ return [];
+ }
+ } else {
+ // remove empty segments
+ return line.filter(x => x !== '');
+ }
+ })
+ // remove empty lines
+ .filter(x => !(Array.isArray(x) && x.length === 0));
+
+ await queryRunner.connection.createQueryBuilder()
+ .update('user_profile')
+ .set({
+ mutedWords: words
+ })
+ .where('userId = :id', { id: entries[i].userId })
+ .execute();
+ }
+ }
+
+ async down(queryRunner) {
+ let entries = await queryRunner.query(`SELECT "userId", "mutedWords" FROM "user_profile"`);
+ for(let i = 0; i < entries.length; i++) {
+ let words = entries[i].mutedWords
+ .map(line => {
+ if (Array.isArray(line)) {
+ return line;
+ } else {
+ // do not split regex at spaces again
+ return [line];
+ }
+ })
+ // remove empty lines
+ .filter(x => !(Array.isArray(x) && x.length === 0));
+
+ await queryRunner.connection.createQueryBuilder()
+ .update('user_profile')
+ .set({
+ mutedWords: words
+ })
+ .where('userId = :id', { id: entries[i].userId })
+ .execute();
+ }
+ }
+}
diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
index e2e871dd2b..dedda3cdf6 100644
--- a/packages/backend/src/misc/check-word-mute.ts
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -11,26 +11,31 @@ type UserLike = {
id: User['id'];
};
-export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
+export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
// 自分自身
if (me && (note.userId === me.id)) return false;
- const words = mutedWords
- // Clean up
- .map(xs => xs.filter(x => x !== ''))
- .filter(xs => xs.length > 0);
-
- if (words.length > 0) {
+ if (mutedWords.length > 0) {
if (note.text == null) return false;
- const matched = words.some(and =>
- and.every(keyword => {
- const regexp = keyword.match(/^\/(.+)\/(.*)$/);
- if (regexp) {
+ const matched = mutedWords.some(filter => {
+ if (Array.isArray(filter)) {
+ return filter.every(keyword => note.text!.includes(keyword));
+ } else {
+ // represents RegExp
+ const regexp = filter.match(/^\/(.+)\/(.*)$/);
+
+ // This should never happen due to input sanitisation.
+ if (!regexp) return false;
+
+ try {
return new RE2(regexp[1], regexp[2]).test(note.text!);
+ } catch (err) {
+ // This should never happen due to input sanitisation.
+ return false;
}
- return note.text!.includes(keyword);
- }));
+ }
+ });
if (matched) return true;
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index eb57aa2bfc..aec7bbd2e1 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -1,3 +1,4 @@
+const RE2 = require('re2');
import $ from 'cafy';
import * as mfm from 'mfm-js';
import { ID } from '@/misc/cafy-id';
@@ -117,7 +118,7 @@ export const meta = {
},
mutedWords: {
- validator: $.optional.arr($.arr($.str)),
+ validator: $.optional.arr($.either($.arr($.str.min(1)).min(1), $.str)),
},
mutedInstances: {
@@ -163,6 +164,12 @@ export const meta = {
code: 'NO_SUCH_PAGE',
id: '8e01b590-7eb9-431b-a239-860e086c408e',
},
+
+ invalidRegexp: {
+ message: 'Invalid Regular Expression.',
+ code: 'INVALID_REGEXP',
+ id: '0d786918-10df-41cd-8f33-8dec7d9a89a5',
+ }
},
res: {
@@ -191,6 +198,18 @@ export default define(meta, async (ps, _user, token) => {
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (ps.mutedWords !== undefined) {
+ // validate regular expression syntax
+ ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
+ const regexp = x.match(/^\/(.+)\/(.*)$/);
+ if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
+
+ try {
+ new RE2(regexp[1], regexp[2]);
+ } catch (err) {
+ throw new ApiError(meta.errors.invalidRegexp);
+ }
+ });
+
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
index 19980dea14..bdccd7f1ee 100644
--- a/packages/client/src/pages/settings/word-mute.vue
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -81,18 +81,65 @@ export default defineComponent({
},
async created() {
- this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
- this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
+ const render = (mutedWords) => mutedWords.map(x => {
+ if (Array.isArray(x)) {
+ return x.join(' ');
+ } else {
+ return x;
+ }
+ }).join('\n');
+
+ this.softMutedWords = render(this.$store.state.mutedWords);
+ this.hardMutedWords = render(this.$i.mutedWords);
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
},
methods: {
async save() {
- this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
+ const parseMutes = (mutes, tab) => {
+ // split into lines, remove empty lines and unnecessary whitespace
+ let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line != '');
+
+ // check each line if it is a RegExp or not
+ for(let i = 0; i < lines.length; i++) {
+ const line = lines[i]
+ const regexp = line.match(/^\/(.+)\/(.*)$/);
+ if (regexp) {
+ // check that the RegExp is valid
+ try {
+ new RegExp(regexp[1], regexp[2]);
+ // note that regex lines will not be split by spaces!
+ } catch (err) {
+ // invalid syntax: do not save, do not reset changed flag
+ os.alert({
+ type: 'error',
+ title: this.$ts.regexpError,
+ text: this.$t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
+ });
+ // re-throw error so these invalid settings are not saved
+ throw err;
+ }
+ } else {
+ lines[i] = line.split(' ');
+ }
+ }
+ };
+
+ let softMutes, hardMutes;
+ try {
+ softMutes = parseMutes(this.softMutedWords, this.$ts._wordMute.soft);
+ hardMutes = parseMutes(this.hardMutedWords, this.$ts._wordMute.hard);
+ } catch (err) {
+ // already displayed error message in parseMutes
+ return;
+ }
+
+ this.$store.set('mutedWords', softMutes);
await os.api('i/update', {
- mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
+ mutedWords: hardMutes,
});
+
this.changed = false;
},
diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts
index 55637bb3b3..74e2581863 100644
--- a/packages/client/src/scripts/check-word-mute.ts
+++ b/packages/client/src/scripts/check-word-mute.ts
@@ -1,23 +1,28 @@
-export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): boolean {
+export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
// 自分自身
if (me && (note.userId === me.id)) return false;
- const words = mutedWords
- // Clean up
- .map(xs => xs.filter(x => x !== ''))
- .filter(xs => xs.length > 0);
-
- if (words.length > 0) {
+ if (mutedWords.length > 0) {
if (note.text == null) return false;
- const matched = words.some(and =>
- and.every(keyword => {
- const regexp = keyword.match(/^\/(.+)\/(.*)$/);
- if (regexp) {
+ const matched = mutedWords.some(filter => {
+ if (Array.isArray(filter)) {
+ return filter.every(keyword => note.text!.includes(keyword));
+ } else {
+ // represents RegExp
+ const regexp = filter.match(/^\/(.+)\/(.*)$/);
+
+ // This should never happen due to input sanitisation.
+ if (!regexp) return false;
+
+ try {
return new RegExp(regexp[1], regexp[2]).test(note.text!);
+ } catch (err) {
+ // This should never happen due to input sanitisation.
+ return false;
}
- return note.text!.includes(keyword);
- }));
+ }
+ });
if (matched) return true;
}