Partially Revert "Hard mute (misskey-dev#12376)"

This reverts commit 864827f788 partially.

型定義に関する実装はそのままに
一部型定義に合わない実装の修正
This commit is contained in:
まっちゃとーにゅ 2023-11-25 12:10:31 +09:00
parent 77e676e121
commit d9efde97e4
No known key found for this signature in database
GPG Key ID: 6AFBBF529601C1DB
17 changed files with 79 additions and 107 deletions

1
locales/index.d.ts vendored
View File

@ -644,7 +644,6 @@ export interface Locale {
"smtpSecureInfo": string; "smtpSecureInfo": string;
"testEmail": string; "testEmail": string;
"wordMute": string; "wordMute": string;
"hardWordMute": string;
"regexpError": string; "regexpError": string;
"regexpErrorDescription": string; "regexpErrorDescription": string;
"instanceMute": string; "instanceMute": string;

View File

@ -641,7 +641,6 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。" smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト" testEmail: "配信テスト"
wordMute: "ワードミュート" wordMute: "ワードミュート"
hardWordMute: "ハードワードミュート"
regexpError: "正規表現エラー" regexpError: "正規表現エラー"
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
instanceMute: "サーバーミュート" instanceMute: "サーバーミュート"

View File

@ -1,11 +1,11 @@
export class HardMute1700383825690 { export class HardMute1700383825690 {
name = 'HardMute1700383825690' name = 'HardMute1700383825690'
async up(queryRunner) { async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
} }
async down(queryRunner) { async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
} }
} }

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RevertHardMute1700880703631 {
name = 'RevertHardMute1700880703631';
async up(queryRunner) {
// migrate hardMutedWords to mutedWords
await queryRunner.query(`
update "user_profile"
set "mutedWords" = (
select jsonb_agg(elem order by ord)
from (
select elem, ord
from (
select elem, row_number() over () as ord
from jsonb_array_elements("mutedWords") as elem
) as muted
union
select elem, 1000000 + row_number() over ()
from jsonb_array_elements("hardMutedWords") as elem
where elem not in (select jsonb_array_elements("mutedWords"))
) as combined
)
where "hardMutedWords" <> '[]'
`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
}
}

View File

@ -473,7 +473,6 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount, unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,
hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances, mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig, notificationRecieveConfig: profile!.notificationRecieveConfig,

View File

@ -217,11 +217,6 @@ export class MiUserProfile {
}) })
public mutedWords: (string[] | string)[]; public mutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],
})
public hardMutedWords: (string[] | string)[];
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],
comment: 'List of instances muted by the user.', comment: 'List of instances muted by the user.',

View File

@ -534,18 +534,6 @@ export const packedMeDetailedOnlySchema = {
}, },
}, },
}, },
hardMutedWords: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
},
mutedInstances: { mutedInstances: {
type: 'array', type: 'array',
nullable: true, optional: false, nullable: true, optional: false,

View File

@ -15,7 +15,6 @@ import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, Pag
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js'; import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js'; import { langmap } from '@/misc/langmap.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -124,11 +123,6 @@ export const meta = {
}, },
} as const; } as const;
const muteWordsType = { type: 'array', items: { oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'string' }
] } } as const;
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
@ -177,8 +171,12 @@ export const paramDef = {
autoSensitive: { type: 'boolean' }, autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: muteWordsType, mutedWords: { type: 'array', items: {
hardMutedWords: muteWordsType, oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'string' }
]
} },
mutedInstances: { type: 'array', items: { mutedInstances: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },
@ -241,19 +239,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
if (ps.mutedWords !== undefined) {
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { const length = ps.mutedWords.length;
const length = mutedWords.length; if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords); throw new ApiError(meta.errors.tooManyMutedWords);
} }
}
function validateMuteWordRegex(mutedWords: (string[] | string)[]) { // validate regular expression syntax
for (const mutedWord of mutedWords) { ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
if (typeof mutedWord !== "string") continue; const regexp = RegExp(/^\/(.+)\/(.*)$/).exec(x as string);
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp); if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try { try {
@ -261,21 +255,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (err) { } catch (err) {
throw new ApiError(meta.errors.invalidRegexp); throw new ApiError(meta.errors.invalidRegexp);
} }
} });
}
if (ps.mutedWords !== undefined) {
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords; profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0; profileUpdates.enableWordMute = ps.mutedWords.length > 0;
} }
if (ps.hardMutedWords !== undefined) {
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;

View File

@ -169,7 +169,6 @@ describe('ユーザー', () => {
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements, unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords, mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances, mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes, mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig, notificationRecieveConfig: user.notificationRecieveConfig,

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div <div
v-if="!hardMuted && !muted" v-if="!muted"
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</article> </article>
</div> </div>
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false"> <div v-else :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name> <template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)"> <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@ -183,7 +183,6 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
mock?: boolean; mock?: boolean;
withHardMute?: boolean;
}>(), { }>(), {
mock: false, mock: false,
}); });
@ -241,7 +240,6 @@ const isLong = shouldCollapsed(appearNote, urls ?? []);
const collapsed = ref(appearNote.cw == null && isLong); const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(checkMute(appearNote, $i?.mutedWords)); const muted = ref(checkMute(appearNote, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true" :ad="true"
:class="$style.notes" :class="$style.notes"
> >
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</div> </div>
</template> </template>

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }"> <template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</template> </template>

View File

@ -9,14 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template> <template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template> <template #label>{{ i18n.ts.wordMute }}</template>
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/> <XWordMute/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
@ -136,7 +129,6 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { $i } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
const renoteMutingPagination = { const renoteMutingPagination = {
@ -215,14 +207,6 @@ async function toggleBlockItem(item) {
} }
} }
async function saveMutedWords(mutedWords: (string | string[])[]) {
await os.api('i/update', { mutedWords });
}
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
await os.api('i/update', { hardMutedWords });
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View File

@ -20,17 +20,10 @@ import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const render = (mutedWords?: (string | string[])[]) => mutedWords?.map(x => {
muted: (string[] | string)[];
}>();
const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) { if (Array.isArray(x)) {
return x.join(' '); return x.join(' ');
} else { } else {
@ -38,7 +31,7 @@ const render = (mutedWords) => mutedWords.map(x => {
} }
}).join('\n'); }).join('\n');
const mutedWords = ref(render(props.muted)); const mutedWords = ref(render($i?.mutedWords));
const changed = ref(false); const changed = ref(false);
watch(mutedWords, () => { watch(mutedWords, () => {
@ -46,14 +39,14 @@ watch(mutedWords, () => {
}); });
async function save() { async function save() {
const parseMutes = (mutes) => { const parseMutes = (mutes?: string): (string | string[])[] => {
// split into lines, remove empty lines and unnecessary whitespace const parsed: (string | string[])[] = [];
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== ''); if (!mutes) return parsed;
// split into lines, remove empty lines and unnecessary whitespace
// check each line if it is a RegExp or not // check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) { for (const [i, line] of mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '').entries()) {
const line = lines[i]; const regexp = RegExp(/^\/(.+)\/(.*)$/).exec(line);
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) { if (regexp) {
// check that the RegExp is valid // check that the RegExp is valid
try { try {
@ -69,15 +62,16 @@ async function save() {
// re-throw error so these invalid settings are not saved // re-throw error so these invalid settings are not saved
throw err; throw err;
} }
parsed.push(line);
} else { } else {
lines[i] = line.split(' '); parsed.push(line.split(' '));
} }
} }
return lines; return parsed;
}; };
let parsed; let parsed: (string | string[])[];
try { try {
parsed = parseMutes(mutedWords.value); parsed = parseMutes(mutedWords.value);
} catch (err) { } catch (err) {
@ -85,7 +79,9 @@ async function save() {
return; return;
} }
emit('save', parsed); await os.api('i/update', {
mutedWords: parsed,
});
changed.value = false; changed.value = false;
} }

View File

@ -1588,7 +1588,6 @@ export type Endpoints = {
receiveAnnouncementEmail?: boolean; receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean; alwaysMarkNsfw?: boolean;
mutedWords?: (string[] | string)[]; mutedWords?: (string[] | string)[];
hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any; notificationRecieveConfig?: any;
emailNotificationTypes?: string[]; emailNotificationTypes?: string[];
alsoKnownAs?: string[]; alsoKnownAs?: string[];
@ -2545,7 +2544,6 @@ type MeDetailed = UserDetailed & {
isDeleted: boolean; isDeleted: boolean;
isExplorable: boolean; isExplorable: boolean;
mutedWords: (string[] | string)[]; mutedWords: (string[] | string)[];
hardMutedWords: (string[] | string)[];
notificationRecieveConfig: { notificationRecieveConfig: {
[notificationType in typeof notificationTypes_2[number]]?: { [notificationType in typeof notificationTypes_2[number]]?: {
type: 'all'; type: 'all';
@ -3084,9 +3082,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// //
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:639:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:638:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:120:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts // src/entities.ts:119:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:638:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:637:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View File

@ -437,7 +437,6 @@ export type Endpoints = {
receiveAnnouncementEmail?: boolean; receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean; alwaysMarkNsfw?: boolean;
mutedWords?: (string[] | string)[]; mutedWords?: (string[] | string)[];
hardMutedWords?: (string[] | string)[];
notificationRecieveConfig?: any; notificationRecieveConfig?: any;
emailNotificationTypes?: string[]; emailNotificationTypes?: string[];
alsoKnownAs?: string[]; alsoKnownAs?: string[];

View File

@ -116,7 +116,6 @@ export type MeDetailed = UserDetailed & {
isDeleted: boolean; isDeleted: boolean;
isExplorable: boolean; isExplorable: boolean;
mutedWords: (string[] | string)[]; mutedWords: (string[] | string)[];
hardMutedWords: (string[] | string)[];
notificationRecieveConfig: { notificationRecieveConfig: {
[notificationType in typeof notificationTypes[number]]?: { [notificationType in typeof notificationTypes[number]]?: {
type: 'all'; type: 'all';