Hard mute (#12376)
* feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count
This commit is contained in:
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export class HardMute1700383825690 {
|
||||
name = 'HardMute1700383825690'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
|
||||
}
|
||||
}
|
@@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||
mutedWords: profile!.mutedWords,
|
||||
hardMutedWords: profile!.hardMutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: [], // 後方互換性のため
|
||||
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||
|
@@ -215,7 +215,12 @@ export class MiUserProfile {
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public mutedWords: string[][];
|
||||
public mutedWords: (string[] | string)[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public hardMutedWords: (string[] | string)[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
|
@@ -530,6 +530,18 @@ 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: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
|
@@ -123,6 +123,11 @@ export const meta = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const muteWords = { type: 'array', items: { oneOf: [
|
||||
{ type: 'array', items: { type: 'string' } },
|
||||
{ type: 'string' }
|
||||
] } } as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -171,7 +176,8 @@ export const paramDef = {
|
||||
autoSensitive: { type: 'boolean' },
|
||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: { type: 'array' },
|
||||
mutedWords: muteWords,
|
||||
hardMutedWords: muteWords,
|
||||
mutedInstances: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -234,16 +240,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||
if (ps.mutedWords !== undefined) {
|
||||
|
||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||
// TODO: ちゃんと数える
|
||||
const length = JSON.stringify(ps.mutedWords).length;
|
||||
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
|
||||
if (length > limit) {
|
||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||
}
|
||||
}
|
||||
|
||||
// validate regular expression syntax
|
||||
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
||||
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
|
||||
for (const mutedWord of mutedWords) {
|
||||
if (typeof mutedWord !== "string") continue;
|
||||
|
||||
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
|
||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||
|
||||
try {
|
||||
@@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
} catch (err) {
|
||||
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.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.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||
|
@@ -168,6 +168,7 @@ describe('ユーザー', () => {
|
||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||
unreadAnnouncements: user.unreadAnnouncements,
|
||||
mutedWords: user.mutedWords,
|
||||
hardMutedWords: user.hardMutedWords,
|
||||
mutedInstances: user.mutedInstances,
|
||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
||||
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||
|
Reference in New Issue
Block a user