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
- インスタンスデフォルトテーマを設定できるように @syuilo
- ミュートに期限を設定できるように @syuilo
- プロフィールの追加情報を最大16まで保存できるように @syuilo
- 連合チャートにPub&Subを追加 @syuilo
- デフォルトで10秒以上時間がかかるデータベースへのクエリは中断されるように @syuilo

View file

@ -834,6 +834,12 @@ searchByGoogle: "ググる"
instanceDefaultLightTheme: "インスタンスデフォルトのライトテーマ"
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
mutePeriod: "ミュートする期限"
indefinitely: "無期限"
tenMinutes: "10分"
oneHour: "1時間"
oneDay: "1日"
oneWeek: "1週間"
_emailUnavailable:
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;
@Index()
@Column('timestamp with time zone', {
nullable: true,
default: null,
})
public expiresAt: Date | null;
@Index()
@Column({
...id(),

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js';
import { Users, Mutings } from '@/models/index.js';
import { MoreThan } from 'typeorm';
import { IsNull, MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js';
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({
where: {
muterId: user.id,
expiresAt: IsNull(),
...(cursor ? { id: MoreThan(cursor) } : {}),
},
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 { resyncCharts } from './resync-charts.js';
import { cleanCharts } from './clean-charts.js';
import { checkExpiredMutings } from './check-expired-mutings.js';
const jobs = {
tickCharts,
resyncCharts,
cleanCharts,
checkExpiredMutings,
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {

View file

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

View file

@ -56,11 +56,44 @@ export function getUserMenu(user) {
}
async function toggleMute() {
os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', {
userId: user.id
}).then(() => {
user.isMuted = !user.isMuted;
});
if (user.isMuted) {
os.apiWithDialog('mute/delete', {
userId: user.id,
}).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() {