Skip to content

Commit

Permalink
feat: 時限ノート機能 (#129)
Browse files Browse the repository at this point in the history
* feat; ScheduledNoteDeleteのQueue関連を実装

* feat: ノート作成時に削除する時間を指定できるように

* feat: ノートに削除予定時刻を含めるように

* feat: APIのレスポンスにノートの削除予定時刻を含めるように

* update: types

* fix: QueueProcessorModuleへの追記漏れを修正

* feat: PostFormに削除予定時刻のエディタを実装

* update: MkDeleteScheduleEditorにタイトルを追加

* feat: 自己消滅するノートにはアイコンを表示するように

* fix: ノートの自己消滅を設定しても下書きが保存されない問題を修正

* fix: BullBoardにscheduledNoteDeleteQueueが表示されない問題を修正
  • Loading branch information
hideki0403 authored Feb 29, 2024
1 parent 77b4470 commit 285087d
Show file tree
Hide file tree
Showing 23 changed files with 785 additions and 404 deletions.
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5240,6 +5240,14 @@ export interface Locale extends ILocale {
* このサーバーからのフォロワーがいないリモートユーザーの、メンションを含むノートをブロックするようにします。
*/
"blockMentionsFromUnfamiliarRemoteUsersDescription": string;
/**
* ノートの自己消滅
*/
"scheduledNoteDelete": string;
/**
* このノートは{time}に消滅します
*/
"noteDeletationAt": ParameterizedString<"time">;
"_bubbleGame": {
/**
* 遊び方
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,8 @@ userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む
enableHorizontalSwipe: "スワイプしてタブを切り替える"
blockMentionsFromUnfamiliarRemoteUsers: "荒らしの可能性があるユーザーからのメンションをブロックする"
blockMentionsFromUnfamiliarRemoteUsersDescription: "このサーバーからのフォロワーがいないリモートユーザーの、メンションを含むノートをブロックするようにします。"
scheduledNoteDelete: "ノートの自己消滅"
noteDeletationAt: "このノートは{time}に消滅します"

_bubbleGame:
howToPlay: "遊び方"
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/migration/1709187210308-scheduled-note-delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class ScheduledNoteDelete1709187210308 {
name = 'ScheduledNoteDelete1709187210308'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "deleteAt" TIMESTAMP WITH TIME ZONE`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "deleteAt"`);
}
}
14 changes: 13 additions & 1 deletion packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type Option = {
uri?: string | null;
url?: string | null;
app?: MiApp | null;
deleteAt?: Date | null;
};

@Injectable()
Expand Down Expand Up @@ -371,7 +372,7 @@ export class NoteCreateService implements OnApplicationShutdown {

const willCauseNotification = mentionedUsers.some(u => u.host === null)
|| (data.visibility === 'specified' && data.visibleUsers?.some(u => u.host === null))
|| data.reply?.userHost === null || (this.isQuote(data) && data.renote?.userHost === null) || false;
|| data.reply?.userHost === null || (this.isQuote(data) && data.renote.userHost === null) || false;

if (meta.blockMentionsFromUnfamiliarRemoteUsers && user.host !== null && willCauseNotification) {
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
Expand Down Expand Up @@ -427,6 +428,7 @@ export class NoteCreateService implements OnApplicationShutdown {
name: data.name,
text: data.text,
hasPoll: data.poll != null,
deleteAt: data.deleteAt,
cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
Expand Down Expand Up @@ -580,6 +582,16 @@ export class NoteCreateService implements OnApplicationShutdown {
});
}

if (data.deleteAt) {
const delay = data.deleteAt.getTime() - Date.now();
this.queueService.scheduledNoteDeleteQueue.add(note.id, {
noteId: note.id,
}, {
delay,
removeOnComplete: true,
});
}

if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);

Expand Down
13 changes: 12 additions & 1 deletion packages/backend/src/core/QueueModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import type { Config } from '@/config.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, ScheduledNoteDeleteJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';

export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type ScheduledNoteDeleteQueue = Bull.Queue<ScheduledNoteDeleteJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
Expand All @@ -33,6 +34,12 @@ const $endedPollNotification: Provider = {
inject: [DI.config],
};

const $scheduledNoteDelete: Provider = {
provide: 'queue:scheduledNoteDelete',
useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULED_NOTE_DELETE, baseQueueOptions(config, QUEUE.SCHEDULED_NOTE_DELETE)),
inject: [DI.config],
};

const $deliver: Provider = {
provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
Expand Down Expand Up @@ -75,6 +82,7 @@ const $webhookDeliver: Provider = {
providers: [
$system,
$endedPollNotification,
$scheduledNoteDelete,
$deliver,
$inbox,
$db,
Expand All @@ -85,6 +93,7 @@ const $webhookDeliver: Provider = {
exports: [
$system,
$endedPollNotification,
$scheduledNoteDelete,
$deliver,
$inbox,
$db,
Expand All @@ -97,6 +106,7 @@ export class QueueModule implements OnApplicationShutdown {
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
Expand All @@ -112,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown {
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),
this.scheduledNoteDeleteQueue.close(),
this.deliverQueue.close(),
this.inboxQueue.close(),
this.dbQueue.close(),
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, ScheduledNoteDeleteQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';

@Injectable()
export class QueueService {
Expand All @@ -26,6 +26,7 @@ export class QueueService {

@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/NoteEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export class NoteEntityService implements OnModuleInit {
}) : undefined,

poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
deleteAt: note.deleteAt?.toISOString() ?? undefined,

...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note, meId, options?._hint_),
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ export class MiNote {
})
public hasPoll: boolean;

@Column('timestamp with time zone', {
nullable: true,
})
public deleteAt: Date | null;

@Index()
@Column({
...id(),
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/json-schema/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ export const packedNoteSchema = {
},
},
},
deleteAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
emojis: {
type: 'object',
optional: true, nullable: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/queue/QueueProcessorModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProsessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
Expand Down Expand Up @@ -73,6 +74,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
RelationshipProcessorService,
WebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
ScheduledNoteDeleteProcessorService,
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/queue/QueueProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProsessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
Expand Down Expand Up @@ -79,6 +80,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
private scheduledNoteDeleteQueueWorker: Bull.Worker;

constructor(
@Inject(DI.config)
Expand All @@ -87,6 +89,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private queueLoggerService: QueueLoggerService,
private webhookDeliverProcessorService: WebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
Expand Down Expand Up @@ -329,6 +332,12 @@ export class QueueProcessorService implements OnApplicationShutdown {
autorun: false,
});
//#endregion

//#region scheduled note delete
this.scheduledNoteDeleteQueueWorker = new Bull.Worker(QUEUE.SCHEDULED_NOTE_DELETE, (job) => this.scheduledNoteDeleteProcessorService.process(job), {
...baseQueueOptions(this.config, QUEUE.SCHEDULED_NOTE_DELETE),
autorun: false,
});
}

@bindThis
Expand All @@ -342,6 +351,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
this.scheduledNoteDeleteQueueWorker.run(),
]);
}

Expand All @@ -356,6 +366,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
this.scheduledNoteDeleteQueueWorker.close(),
]);
}

Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/queue/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const QUEUE = {
INBOX: 'inbox',
SYSTEM: 'system',
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete',
DB: 'db',
RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { ScheduledNoteDeleteJobData } from '../types.js';

@Injectable()
export class ScheduledNoteDeleteProcessorService {
private logger: Logger;

constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private noteDeleteService: NoteDeleteService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('scheduled-note-delete');
}

@bindThis
public async process(job: Bull.Job<ScheduledNoteDeleteJobData>): Promise<void> {
const note = await this.notesRepository.findOneBy({ id: job.data.noteId });
if (note == null) {
return;
}

const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) {
return;
}

await this.noteDeleteService.delete(user, note);
this.logger.info(`Deleted note ${note.id}`);
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/queue/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};

export type ScheduledNoteDeleteJobData = {
noteId: MiNote['id'];
};

export type WebhookDeliverJobData = {
type: string;
content: unknown;
Expand Down
25 changes: 25 additions & 0 deletions packages/backend/src/server/api/endpoints/notes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export const meta = {
id: '04da457d-b083-4055-9082-955525eda5a5',
},

cannotScheduleDeleteEarlierThanNow: {
message: 'Scheduled delete time is earlier than now.',
code: 'CANNOT_SCHEDULE_DELETE_EARLIER_THAN_NOW',
id: '05655b05-5a09-47c3-af56-de0c0900791a',
},

noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
Expand Down Expand Up @@ -184,6 +190,14 @@ export const paramDef = {
},
required: ['choices'],
},
scheduledDelete: {
type: 'object',
nullable: true,
properties: {
deleteAt: { type: 'integer', nullable: true },
deleteAfter: { type: 'integer', nullable: true, minimum: 1 },
},
},
},
// (re)note with text, files and poll are optional
if: {
Expand Down Expand Up @@ -344,6 +358,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}

if (ps.scheduledDelete) {
if (typeof ps.scheduledDelete.deleteAt === 'number') {
if (ps.scheduledDelete.deleteAt < Date.now()) {
throw new ApiError(meta.errors.cannotScheduleDeleteEarlierThanNow);
}
} else if (typeof ps.scheduledDelete.deleteAfter === 'number') {
ps.scheduledDelete.deleteAt = Date.now() + ps.scheduledDelete.deleteAfter;
}
}

let channel: MiChannel | null = null;
if (ps.channelId != null) {
channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
Expand Down Expand Up @@ -375,6 +399,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
deleteAt: ps.scheduledDelete?.deleteAt ? new Date(ps.scheduledDelete.deleteAt) : null,
});

if (note == null) {
Expand Down
Loading

0 comments on commit 285087d

Please sign in to comment.