parent
36657836f7
commit
455cce801d
10
locales/index.d.ts
vendored
10
locales/index.d.ts
vendored
@ -5266,6 +5266,14 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"gameRetry": string;
|
||||
/**
|
||||
* すぐ消す
|
||||
*/
|
||||
"scheduledNoteDelete": string;
|
||||
/**
|
||||
* このノートは{time}に消去されます
|
||||
*/
|
||||
"noteDeletationAt": ParameterizedString<"time">;
|
||||
/**
|
||||
* 使用しない場合は空欄にしてください
|
||||
*/
|
||||
"notUsePleaseLeaveBlank": string;
|
||||
@ -5411,6 +5419,8 @@ export interface Locale extends ILocale {
|
||||
"section3": string;
|
||||
};
|
||||
};
|
||||
"autoRejectFollowRequest": string;
|
||||
"autoRejectFollowRequestDescription": string;
|
||||
"_announcement": {
|
||||
/**
|
||||
* 既存ユーザーのみ
|
||||
|
@ -1337,6 +1337,8 @@ _delivery:
|
||||
manuallySuspended: "手動停止中"
|
||||
goneSuspended: "サーバー削除のため停止中"
|
||||
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
|
||||
scheduledNoteDelete: "すぐ消す"
|
||||
noteDeletationAt: "このノートは{time}に削除されます"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
@ -1353,6 +1355,8 @@ _bubbleGame:
|
||||
section1: "位置を調整してハコにモノを落とします。"
|
||||
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
||||
autoRejectFollowRequest: "フォローリクエストを自動で拒否する"
|
||||
autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようにします。「フォロー中ユーザーからのフォロリクを自動承認」がONになっている場合は、フォロー中ユーザーからのフォローリクエストは自動的に承認され、それ以外のユーザーからのフォローリクエストは自動的に拒否されるようになります。"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -0,0 +1,11 @@
|
||||
export class FeatAutoRejectFollowRequest1697683129062 {
|
||||
name = 'FeatAutoRejectFollowRequest1697683129062'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoRejectFollowRequest" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoRejectFollowRequest"`);
|
||||
}
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@ import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { Data } from 'ws';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@ -148,6 +149,7 @@ type Option = {
|
||||
uri?: string | null;
|
||||
url?: string | null;
|
||||
app?: MiApp | null;
|
||||
deleteAt?: Date | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -439,6 +441,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,
|
||||
@ -614,6 +617,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);
|
||||
|
||||
|
@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common';
|
||||
|
||||
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;
|
||||
@ -41,6 +42,12 @@ const $endedPollNotification: Provider = {
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $scheduledNoteDeleted: 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)),
|
||||
@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = {
|
||||
providers: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduledNoteDeleted,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = {
|
||||
exports: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduledNoteDeleted,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
@ -113,6 +122,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,
|
||||
@ -129,6 +139,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(),
|
||||
|
@ -45,6 +45,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,
|
||||
|
@ -214,6 +214,20 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (!autoAccept) {
|
||||
// autoAcceptが無効かつautoRejectが有効な場合はフォローリクエストを拒否する
|
||||
if (this.userEntityService.isLocalUser(followee) && followeeProfile.autoRejectFollowRequest) {
|
||||
if (this.userEntityService.isRemoteUser(follower)) {
|
||||
// リモートからならRejectを返す
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||
}
|
||||
|
||||
// ローカルユーザーに対しては敢えてpublishUnfollowせずにお茶を濁す
|
||||
// フォローできない不具合と勘違いされたりフォローリクエストを連打される可能性があるため
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.createFollowRequest(follower, followee, requestId, withReplies);
|
||||
return;
|
||||
}
|
||||
@ -579,7 +593,9 @@ export class UserFollowingService implements OnModuleInit {
|
||||
});
|
||||
|
||||
if (!requestExist) {
|
||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||
// throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||
// 本来ならエラーを返すが、フォローリクエストの自動拒否機能の関係上、エラーを返さずに無視する
|
||||
return;
|
||||
}
|
||||
|
||||
await this.followRequestsRepository.delete({
|
||||
|
@ -395,6 +395,13 @@ export class NoteEntityService implements OnModuleInit {
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : 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_),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
|
||||
|
@ -620,6 +620,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
autoSensitive: profile!.autoSensitive,
|
||||
carefulBot: profile!.carefulBot,
|
||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
||||
autoRejectFollowRequest: profile!.autoRejectFollowRequest,
|
||||
noCrawle: profile!.noCrawle,
|
||||
preventAiLearning: profile!.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
|
@ -188,6 +188,11 @@ export class MiNote {
|
||||
})
|
||||
public hasPoll: boolean;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public deleteAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
@ -172,6 +172,11 @@ export class MiUserProfile {
|
||||
})
|
||||
public autoAcceptFollowed: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoRejectFollowRequest: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether reject index by crawler.',
|
||||
|
@ -152,6 +152,11 @@ export const packedNoteSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
deleteAt: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
emojis: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
|
@ -41,6 +41,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js';
|
||||
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
UserWebhookDeliverProcessorService,
|
||||
SystemWebhookDeliverProcessorService,
|
||||
EndedPollNotificationProcessorService,
|
||||
ScheduledNoteDeleteProcessorService,
|
||||
DeliverProcessorService,
|
||||
InboxProcessorService,
|
||||
AggregateRetentionProcessorService,
|
||||
|
@ -45,6 +45,7 @@ import { AggregateRetentionProcessorService } from './processors/AggregateRetent
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QUEUE, baseQueueOptions } from './const.js';
|
||||
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
|
||||
import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js';
|
||||
|
||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||
function httpRelatedBackoff(attemptsMade: number) {
|
||||
@ -84,6 +85,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)
|
||||
@ -93,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService,
|
||||
private deliverProcessorService: DeliverProcessorService,
|
||||
private inboxProcessorService: InboxProcessorService,
|
||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
@ -513,6 +516,12 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
//#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
|
||||
@ -527,6 +536,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
this.relationshipQueueWorker.run(),
|
||||
this.objectStorageQueueWorker.run(),
|
||||
this.endedPollNotificationQueueWorker.run(),
|
||||
this.scheduledNoteDeleteQueueWorker.run(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -542,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
this.relationshipQueueWorker.close(),
|
||||
this.objectStorageQueueWorker.close(),
|
||||
this.endedPollNotificationQueueWorker.close(),
|
||||
this.scheduledNoteDeleteQueueWorker.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -0,0 +1,43 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
@ -131,6 +131,10 @@ export type EndedPollNotificationJobData = {
|
||||
noteId: MiNote['id'];
|
||||
};
|
||||
|
||||
export type ScheduledNoteDeleteJobData = {
|
||||
noteId: MiNote['id'];
|
||||
}
|
||||
|
||||
export type SystemWebhookDeliverJobData = {
|
||||
type: string;
|
||||
content: unknown;
|
||||
|
@ -183,6 +183,7 @@ export const paramDef = {
|
||||
publicReactions: { type: 'boolean' },
|
||||
carefulBot: { type: 'boolean' },
|
||||
autoAcceptFollowed: { type: 'boolean' },
|
||||
autoRejectFollowRequest: { type: 'boolean' },
|
||||
noCrawle: { type: 'boolean' },
|
||||
preventAiLearning: { type: 'boolean' },
|
||||
noindex: { type: 'boolean' },
|
||||
@ -337,6 +338,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||
if (typeof ps.autoRejectFollowRequest === 'boolean') profileUpdates.autoRejectFollowRequest = ps.autoRejectFollowRequest;
|
||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
|
||||
|
@ -104,6 +104,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: '9576c3c8-d8f3-11ee-ac15-00155d19d35d',
|
||||
},
|
||||
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
@ -197,6 +203,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: {
|
||||
@ -365,6 +379,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 });
|
||||
@ -396,6 +420,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,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -30,7 +30,7 @@ import type {
|
||||
DeliverQueue,
|
||||
EndedPollNotificationQueue,
|
||||
InboxQueue,
|
||||
ObjectStorageQueue,
|
||||
ObjectStorageQueue, ScheduledNoteDeleteQueue,
|
||||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
@ -116,6 +116,7 @@ export class ClientServerService {
|
||||
|
||||
@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,
|
||||
@ -248,6 +249,7 @@ export class ClientServerService {
|
||||
queues: [
|
||||
this.systemQueue,
|
||||
this.endedPollNotificationQueue,
|
||||
this.scheduledNoteDeleteQueue,
|
||||
this.deliverQueue,
|
||||
this.inboxQueue,
|
||||
this.dbQueue,
|
||||
|
165
packages/frontend/src/components/MkDeleteScheduleEditor.vue
Normal file
165
packages/frontend/src/components/MkDeleteScheduleEditor.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="zmdxowus">
|
||||
<span>{{ i18n.ts.scheduledNoteDelete }}</span>
|
||||
<section>
|
||||
<div>
|
||||
<MkSelect v-model="expiration" small>
|
||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||
<option value="at">{{ i18n.ts._poll.at }}</option>
|
||||
<option value="after">{{ i18n.ts._poll.after }}</option>
|
||||
</MkSelect>
|
||||
<section v-if="expiration === 'at'">
|
||||
<MkInput v-model="atDate" small type="date" class="input">
|
||||
<template #label>{{ i18n.ts._poll.deadlineDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="atTime" small type="time" class="input">
|
||||
<template #label>{{ i18n.ts._poll.deadlineTime }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section v-else-if="expiration === 'after'">
|
||||
<MkInput v-model="after" small type="number" class="input">
|
||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
<option value="second">{{ i18n.ts._time.second }}</option>
|
||||
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
||||
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
||||
<option value="day">{{ i18n.ts._time.day }}</option>
|
||||
</MkSelect>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||
import { addTime } from '@/scripts/time.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export type DeleteScheduleEditorModelValue = {
|
||||
deleteAt: number | null;
|
||||
deleteAfter: number | null;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: DeleteScheduleEditorModelValue;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: DeleteScheduleEditorModelValue): void;
|
||||
}>();
|
||||
|
||||
const expiration = ref('at');
|
||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||
const atTime = ref('00:00');
|
||||
const after = ref(0);
|
||||
const unit = ref('second');
|
||||
|
||||
if (props.modelValue.deleteAt) {
|
||||
expiration.value = 'at';
|
||||
const deleteAt = new Date(props.modelValue.deleteAt);
|
||||
atDate.value = formatDateTimeString(deleteAt, 'yyyy-MM-dd');
|
||||
atTime.value = formatDateTimeString(deleteAt, 'HH:mm');
|
||||
} else if (typeof props.modelValue.deleteAfter === 'number') {
|
||||
expiration.value = 'after';
|
||||
after.value = props.modelValue.deleteAfter / 1000;
|
||||
}
|
||||
|
||||
function get(): DeleteScheduleEditorModelValue {
|
||||
const calcAt = () => {
|
||||
return new Date(`${atDate.value} ${atTime.value}`).getTime();
|
||||
};
|
||||
|
||||
const calcAfter = () => {
|
||||
let base = parseInt(after.value.toString());
|
||||
switch (unit.value) {
|
||||
// @ts-expect-error fallthrough
|
||||
case 'day': base *= 24;
|
||||
// @ts-expect-error fallthrough
|
||||
case 'hour': base *= 60;
|
||||
// @ts-expect-error fallthrough
|
||||
case 'minute': base *= 60;
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'second': return base *= 1000;
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
deleteAt: expiration.value === 'at' ? calcAt() : null,
|
||||
deleteAfter: expiration.value === 'after' ? calcAfter() : null,
|
||||
};
|
||||
}
|
||||
|
||||
watch([expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zmdxowus {
|
||||
padding: 8px 16px;
|
||||
|
||||
>span {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
>ul {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
>li {
|
||||
display: flex;
|
||||
margin: 8px 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
>.input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
>button {
|
||||
width: 32px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>section {
|
||||
margin: 16px 0 0 0;
|
||||
|
||||
>div {
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
&:last-child {
|
||||
flex: 1 0 auto;
|
||||
|
||||
>div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
>section {
|
||||
// MAGIC: Prevent div above from growing unless wrapped to its own line
|
||||
flex-grow: 9999;
|
||||
align-items: end;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
>.input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -116,6 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkA :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
<span v-if="appearNote.deleteAt"><i class="ti ti-bomb"></i>{{ i18n.ts.scheduledNoteDelete }}: <MkTime :time="appearNote.deleteAt" mode="detail" colored/></span>
|
||||
</div>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||
|
@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span>
|
||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||
<span v-if="note.deleteAt" style="margin-left: 0.5em;" :title="i18n.tsx.noteDeletationAt({ time: dateTimeFormat.format(new Date(note.deleteAt)) })"><i class="ti ti-bomb"></i></span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@ -43,6 +44,7 @@ import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
@ -74,6 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkDeleteScheduleEditor v-if="scheduledNoteDelete" v-model="scheduledNoteDelete" @destroyed="scheduledNoteDelete = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
</div>
|
||||
@ -81,6 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.footerLeft">
|
||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||
<button v-tooltip="i18n.ts.scheduledNoteDelete" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: scheduledNoteDelete }]" @click="toggleScheduledNoteDelete"><i class="ti ti-bomb"></i></button>
|
||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||
@ -110,6 +112,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||
import MkDeleteScheduleEditor, { type DeleteScheduleEditorModelValue } from '@/components/MkDeleteScheduleEditor.vue';
|
||||
import { host, url } from '@/config.js';
|
||||
import { erase, unique } from '@/scripts/array.js';
|
||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||
@ -182,6 +185,7 @@ const posted = ref(false);
|
||||
const text = ref(props.initialText ?? '');
|
||||
const files = ref(props.initialFiles ?? []);
|
||||
const poll = ref<PollEditorModelValue | null>(null);
|
||||
const scheduledNoteDelete = ref<DeleteScheduleEditorModelValue | null>(null);
|
||||
const useCw = ref<boolean>(!!props.initialCw);
|
||||
const showPreview = ref(defaultStore.state.showPreview);
|
||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||
@ -366,6 +370,7 @@ function watchForDraft() {
|
||||
watch(useCw, () => saveDraft());
|
||||
watch(cw, () => saveDraft());
|
||||
watch(poll, () => saveDraft());
|
||||
watch(scheduledNoteDelete, () => saveDraft());
|
||||
watch(files, () => saveDraft(), { deep: true });
|
||||
watch(visibility, () => saveDraft());
|
||||
watch(localOnly, () => saveDraft());
|
||||
@ -421,6 +426,17 @@ function togglePoll() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleScheduledNoteDelete() {
|
||||
if (scheduledNoteDelete.value) {
|
||||
scheduledNoteDelete.value = null;
|
||||
} else {
|
||||
scheduledNoteDelete.value = {
|
||||
deleteAt: null,
|
||||
deleteAfter: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addTag(tag: string) {
|
||||
insertTextAtCursor(textareaEl.value, ` #${tag} `);
|
||||
}
|
||||
@ -718,6 +734,7 @@ function saveDraft() {
|
||||
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
|
||||
quoteId: quoteId.value,
|
||||
reactionAcceptance: reactionAcceptance.value,
|
||||
scheduledNoteDelete: scheduledNoteDelete.value,
|
||||
},
|
||||
};
|
||||
|
||||
@ -794,6 +811,7 @@ async function post(ev?: MouseEvent) {
|
||||
renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
|
||||
channelId: props.channel ? props.channel.id : undefined,
|
||||
poll: poll.value,
|
||||
scheduledDelete: scheduledNoteDelete.value,
|
||||
cw: useCw.value ? cw.value ?? '' : null,
|
||||
localOnly: localOnly.value,
|
||||
visibility: visibility.value,
|
||||
@ -1029,6 +1047,9 @@ onMounted(() => {
|
||||
}
|
||||
quoteId.value = draft.data.quoteId;
|
||||
reactionAcceptance.value = draft.data.reactionAcceptance;
|
||||
if (draft.data.scheduledNoteDelete) {
|
||||
scheduledNoteDelete.value = draft.data.scheduledNoteDelete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1049,6 +1070,12 @@ onMounted(() => {
|
||||
expiredAfter: null,
|
||||
};
|
||||
}
|
||||
if (init.deleteAt) {
|
||||
scheduledNoteDelete.value = {
|
||||
deleteAt: init.deleteAt ? (new Date(init.deleteAt)).getTime() : null,
|
||||
deleteAfter: null,
|
||||
};
|
||||
}
|
||||
if (init.visibleUserIds) {
|
||||
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
|
@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="isLocked" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
|
||||
<MkSwitch v-if="isLocked" v-model="autoAcceptFollowed" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</MkSwitch>
|
||||
<MkSwitch v-if="isLocked" v-model="autoRejectFollowRequest" @update:modelValue="save()">
|
||||
{{ i18n.ts.autoRejectFollowRequest }}<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
<template #caption>{{ i18n.ts.autoRejectFollowRequestDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
|
||||
{{ i18n.ts.makeReactionsPublic }}
|
||||
@ -87,6 +91,7 @@ const $i = signinRequired();
|
||||
|
||||
const isLocked = ref($i.isLocked);
|
||||
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
|
||||
const autoRejectFollowRequest = ref($i.autoRejectFollowRequest);
|
||||
const noCrawle = ref($i.noCrawle);
|
||||
const noindex = ref($i.noindex);
|
||||
const isExplorable = ref($i.isExplorable);
|
||||
@ -104,6 +109,7 @@ function save() {
|
||||
misskeyApi('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
autoAcceptFollowed: !!autoAcceptFollowed.value,
|
||||
autoRejectFollowRequest: !!autoRejectFollowRequest,
|
||||
noCrawle: !!noCrawle.value,
|
||||
noindex: !!noindex.value,
|
||||
isExplorable: !!isExplorable.value,
|
||||
|
@ -4145,7 +4145,7 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
icon: 'info' | 'warning' | 'error' | 'success';
|
||||
/** @enum {string} */
|
||||
display: 'dialog' | 'normal' | 'banner';
|
||||
display: 'dialog' | 'normal' | 'banner' | 'emergency';
|
||||
needConfirmationToRead: boolean;
|
||||
silence: boolean;
|
||||
forYou: boolean;
|
||||
@ -4204,6 +4204,8 @@ export type components = {
|
||||
votes: number;
|
||||
}[];
|
||||
}) | null;
|
||||
/** Format: date-time */
|
||||
deleteAt?: string | null;
|
||||
emojis?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
@ -21897,6 +21899,10 @@ export type operations = {
|
||||
expiresAt?: number | null;
|
||||
expiredAfter?: number | null;
|
||||
}) | null;
|
||||
scheduledDelete?: ({
|
||||
deleteAt?: number | null;
|
||||
deleteAfter?: number | null;
|
||||
}) | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user