13
									
								
								packages/backend/migration/1646387162108-mute-expires-at.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1646387162108-mute-expires-at.js
									
									
									
									
									
										Normal 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"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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(),
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -273,6 +273,11 @@ export default function() {
 | 
			
		||||
		repeat: { cron: '0 0 * * *' },
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	systemQueue.add('checkExpiredMutings', {
 | 
			
		||||
	}, {
 | 
			
		||||
		repeat: { cron: '*/5 * * * *' },
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	processSystemQueue(systemQueue);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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>>) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user