feat: 時限ミュート

#7677
This commit is contained in:
syuilo 2022-03-04 20:23:53 +09:00
parent 5ab7362d4c
commit 17f7b41240
12 changed files with 116 additions and 6 deletions

View file

@ -20,6 +20,7 @@ You should also include the user name that made the change.
### Improvements ### Improvements
- インスタンスデフォルトテーマを設定できるように @syuilo - インスタンスデフォルトテーマを設定できるように @syuilo
- ミュートに期限を設定できるように @syuilo
- プロフィールの追加情報を最大16まで保存できるように @syuilo - プロフィールの追加情報を最大16まで保存できるように @syuilo
- 連合チャートにPub&Subを追加 @syuilo - 連合チャートにPub&Subを追加 @syuilo
- デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo - デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo

View file

@ -834,6 +834,12 @@ searchByGoogle: "ググる"
instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ" instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ"
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ" instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
mutePeriod: "ミュートする期限"
indefinitely: "無期限"
tenMinutes: "10分"
oneHour: "1時間"
oneDay: "1日"
oneWeek: "1週間"
_emailUnavailable: _emailUnavailable:
used: "既に使用されています" used: "既に使用されています"

View file

@ -0,0 +1,13 @@
export class muteExpiresAt1646387162108 {
name = 'muteExpiresAt1646387162108'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "muting" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`CREATE INDEX "IDX_c1fd1c3dfb0627aa36c253fd14" ON "muting" ("expiresAt") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_c1fd1c3dfb0627aa36c253fd14"`);
await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "expiresAt"`);
}
}

View file

@ -14,6 +14,13 @@ export class Muting {
}) })
public createdAt: Date; public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
default: null,
})
public expiresAt: Date | null;
@Index() @Index()
@Column({ @Column({
...id(), ...id(),

View file

@ -16,6 +16,7 @@ export class MutingRepository extends Repository<Muting> {
return await awaitAll({ return await awaitAll({
id: muting.id, id: muting.id,
createdAt: muting.createdAt.toISOString(), createdAt: muting.createdAt.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId, muteeId: muting.muteeId,
mutee: Users.pack(muting.muteeId, me, { mutee: Users.pack(muting.muteeId, me, {
detail: true, detail: true,

View file

@ -12,6 +12,11 @@ export const packedMutingSchema = {
optional: false, nullable: false, optional: false, nullable: false,
format: 'date-time', format: 'date-time',
}, },
expiresAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
muteeId: { muteeId: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -273,6 +273,11 @@ export default function() {
repeat: { cron: '0 0 * * *' }, repeat: { cron: '0 0 * * *' },
}); });
systemQueue.add('checkExpiredMutings', {
}, {
repeat: { cron: '*/5 * * * *' },
});
processSystemQueue(systemQueue); processSystemQueue(systemQueue);
} }

View file

@ -7,7 +7,7 @@ import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js'; import { getFullApAccount } from '@/misc/convert-host.js';
import { Users, Mutings } from '@/models/index.js'; import { Users, Mutings } from '@/models/index.js';
import { MoreThan } from 'typeorm'; import { IsNull, MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
const logger = queueLogger.createSubLogger('export-mute'); const logger = queueLogger.createSubLogger('export-mute');
@ -40,6 +40,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
const mutes = await Mutings.find({ const mutes = await Mutings.find({
where: { where: {
muterId: user.id, muterId: user.id,
expiresAt: IsNull(),
...(cursor ? { id: MoreThan(cursor) } : {}), ...(cursor ? { id: MoreThan(cursor) } : {}),
}, },
take: 100, take: 100,

View file

@ -0,0 +1,30 @@
import Bull from 'bull';
import { In } from 'typeorm';
import { Mutings } from '@/models/index.js';
import { queueLogger } from '../../logger.js';
import { publishUserEvent } from '@/services/stream.js';
const logger = queueLogger.createSubLogger('check-expired-mutings');
export async function checkExpiredMutings(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
logger.info(`Checking expired mutings...`);
const expired = await Mutings.createQueryBuilder('muting')
.where('muting.expiresAt IS NOT NULL')
.andWhere('muting.expiresAt < :now', { now: new Date() })
.innerJoinAndSelect('muting.mutee', 'mutee')
.getMany();
if (expired.length > 0) {
await Mutings.delete({
id: In(expired.map(m => m.id)),
});
for (const m of expired) {
publishUserEvent(m.muterId, 'unmute', m.mutee!);
}
}
logger.succ(`All expired mutings checked.`);
done();
}

View file

@ -2,11 +2,13 @@ import Bull from 'bull';
import { tickCharts } from './tick-charts.js'; import { tickCharts } from './tick-charts.js';
import { resyncCharts } from './resync-charts.js'; import { resyncCharts } from './resync-charts.js';
import { cleanCharts } from './clean-charts.js'; import { cleanCharts } from './clean-charts.js';
import { checkExpiredMutings } from './check-expired-mutings.js';
const jobs = { const jobs = {
tickCharts, tickCharts,
resyncCharts, resyncCharts,
cleanCharts, cleanCharts,
checkExpiredMutings,
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>; } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) { export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {

View file

@ -38,6 +38,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
expiresAt: { type: 'integer', nullable: true },
}, },
required: ['userId'], required: ['userId'],
} as const; } as const;
@ -67,10 +68,15 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.alreadyMuting); throw new ApiError(meta.errors.alreadyMuting);
} }
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
return;
}
// Create mute // Create mute
await Mutings.insert({ await Mutings.insert({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
muterId: muter.id, muterId: muter.id,
muteeId: mutee.id, muteeId: mutee.id,
} as Muting); } as Muting);

View file

@ -56,11 +56,44 @@ export function getUserMenu(user) {
} }
async function toggleMute() { async function toggleMute() {
os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', { if (user.isMuted) {
userId: user.id os.apiWithDialog('mute/delete', {
}).then(() => { userId: user.id,
user.isMuted = !user.isMuted; }).then(() => {
}); user.isMuted = false;
});
} else {
const { canceled, result: period } = await os.select({
title: i18n.ts.mutePeriod,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', text: i18n.ts.tenMinutes,
}, {
value: 'oneHour', text: i18n.ts.oneHour,
}, {
value: 'oneDay', text: i18n.ts.oneDay,
}, {
value: 'oneWeek', text: i18n.ts.oneWeek,
}],
default: 'indefinitely',
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: null;
os.apiWithDialog('mute/create', {
userId: user.id,
expiresAt,
}).then(() => {
user.isMuted = true;
});
}
} }
async function toggleBlock() { async function toggleBlock() {