mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	merge: Schedule Notes (!804)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/804 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Hazelnoot <acomputerdog@gmail.com>
This commit is contained in:
		
						commit
						8eb9c20df7
					
				
					 47 changed files with 1616 additions and 16 deletions
				
			
		
							
								
								
									
										28
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -6947,6 +6947,10 @@ export interface Locale extends ILocale {
 | 
			
		|||
             * Can import notes
 | 
			
		||||
             */
 | 
			
		||||
            "canImportNotes": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * Maximum number of scheduled notes
 | 
			
		||||
             */
 | 
			
		||||
            "scheduleNoteMax": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_condition": {
 | 
			
		||||
            /**
 | 
			
		||||
| 
						 | 
				
			
			@ -8416,6 +8420,14 @@ export interface Locale extends ILocale {
 | 
			
		|||
         * 違反を報告する
 | 
			
		||||
         */
 | 
			
		||||
        "write:report-abuse": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * View your list of scheduled notes
 | 
			
		||||
         */
 | 
			
		||||
        "read:notes-schedule": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * Compose or delete scheduled notes
 | 
			
		||||
         */
 | 
			
		||||
        "write:notes-schedule": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_auth": {
 | 
			
		||||
        /**
 | 
			
		||||
| 
						 | 
				
			
			@ -9546,6 +9558,14 @@ export interface Locale extends ILocale {
 | 
			
		|||
         * Note got edited
 | 
			
		||||
         */
 | 
			
		||||
        "edited": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * Posting scheduled note failed
 | 
			
		||||
         */
 | 
			
		||||
        "scheduledNoteFailed": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * Scheduled Note was posted
 | 
			
		||||
         */
 | 
			
		||||
        "scheduledNotePosted": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_deck": {
 | 
			
		||||
        /**
 | 
			
		||||
| 
						 | 
				
			
			@ -11428,6 +11448,14 @@ export interface Locale extends ILocale {
 | 
			
		|||
     * Select a follow relationship...
 | 
			
		||||
     */
 | 
			
		||||
    "selectFollowRelationship": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * Schedule a note
 | 
			
		||||
     */
 | 
			
		||||
    "schedulePost": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * List of scheduled notes
 | 
			
		||||
     */
 | 
			
		||||
    "schedulePostList": string;
 | 
			
		||||
}
 | 
			
		||||
declare const locales: {
 | 
			
		||||
    [lang: string]: Locale;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								packages/backend/migration/1699437894737-scheduleNote.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/backend/migration/1699437894737-scheduleNote.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class ScheduleNote1699437894737 {
 | 
			
		||||
    name = 'ScheduleNote1699437894737'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, "scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_e798958c40009bf0cdef4f28b5" ON "note_schedule" ("userId") `);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "note_schedule"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import {
 | 
			
		|||
	RelationshipJobData,
 | 
			
		||||
	UserWebhookDeliverJobData,
 | 
			
		||||
	SystemWebhookDeliverJobData,
 | 
			
		||||
	ScheduleNotePostJobData,
 | 
			
		||||
} from '../queue/types.js';
 | 
			
		||||
import type { Provider } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,7 @@ export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
 | 
			
		|||
export type ObjectStorageQueue = Bull.Queue;
 | 
			
		||||
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
 | 
			
		||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
 | 
			
		||||
export type ScheduleNotePostQueue = Bull.Queue<ScheduleNotePostJobData>;
 | 
			
		||||
 | 
			
		||||
const $system: Provider = {
 | 
			
		||||
	provide: 'queue:system',
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +85,12 @@ const $systemWebhookDeliver: Provider = {
 | 
			
		|||
	inject: [DI.config],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $scheduleNotePost: Provider = {
 | 
			
		||||
	provide: 'queue:scheduleNotePost',
 | 
			
		||||
	useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST)),
 | 
			
		||||
	inject: [DI.config],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [
 | 
			
		||||
	],
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +104,7 @@ const $systemWebhookDeliver: Provider = {
 | 
			
		|||
		$objectStorage,
 | 
			
		||||
		$userWebhookDeliver,
 | 
			
		||||
		$systemWebhookDeliver,
 | 
			
		||||
		$scheduleNotePost,
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		$system,
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +116,7 @@ const $systemWebhookDeliver: Provider = {
 | 
			
		|||
		$objectStorage,
 | 
			
		||||
		$userWebhookDeliver,
 | 
			
		||||
		$systemWebhookDeliver,
 | 
			
		||||
		$scheduleNotePost,
 | 
			
		||||
	],
 | 
			
		||||
})
 | 
			
		||||
export class QueueModule implements OnApplicationShutdown {
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +130,7 @@ export class QueueModule implements OnApplicationShutdown {
 | 
			
		|||
		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
			
		||||
		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
 | 
			
		||||
	) {}
 | 
			
		||||
 | 
			
		||||
	public async dispose(): Promise<void> {
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +147,7 @@ export class QueueModule implements OnApplicationShutdown {
 | 
			
		|||
			this.objectStorageQueue.close(),
 | 
			
		||||
			this.userWebhookDeliverQueue.close(),
 | 
			
		||||
			this.systemWebhookDeliverQueue.close(),
 | 
			
		||||
			this.scheduleNotePostQueue.close(),
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ import type {
 | 
			
		|||
	SystemQueue,
 | 
			
		||||
	UserWebhookDeliverQueue,
 | 
			
		||||
	SystemWebhookDeliverQueue,
 | 
			
		||||
	ScheduleNotePostQueue,
 | 
			
		||||
} from './QueueModule.js';
 | 
			
		||||
import type httpSignature from '@peertube/http-signature';
 | 
			
		||||
import type * as Bull from 'bullmq';
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +53,7 @@ export class QueueService {
 | 
			
		|||
		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
			
		||||
		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue,
 | 
			
		||||
	) {
 | 
			
		||||
		this.systemQueue.add('tickCharts', {
 | 
			
		||||
		}, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ export type RolePolicies = {
 | 
			
		|||
	ltlAvailable: boolean;
 | 
			
		||||
	btlAvailable: boolean;
 | 
			
		||||
	canPublicNote: boolean;
 | 
			
		||||
	scheduleNoteMax: number;
 | 
			
		||||
	mentionLimit: number;
 | 
			
		||||
	canInvite: boolean;
 | 
			
		||||
	inviteLimit: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +73,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		|||
	ltlAvailable: true,
 | 
			
		||||
	btlAvailable: false,
 | 
			
		||||
	canPublicNote: true,
 | 
			
		||||
	scheduleNoteMax: 5,
 | 
			
		||||
	mentionLimit: 20,
 | 
			
		||||
	canInvite: false,
 | 
			
		||||
	inviteLimit: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -377,6 +379,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		|||
			btlAvailable: calc('btlAvailable', vs => vs.some(v => v === true)),
 | 
			
		||||
			ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
 | 
			
		||||
			canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
 | 
			
		||||
			scheduleNoteMax: calc('scheduleNoteMax', vs => Math.max(...vs)),
 | 
			
		||||
			mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
 | 
			
		||||
			canInvite: calc('canInvite', vs => vs.some(v => v === true)),
 | 
			
		||||
			inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common';
 | 
			
		|||
import type { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
import type { NoteEntityService } from './NoteEntityService.js';
 | 
			
		||||
 | 
			
		||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited'] as (typeof groupedNotificationTypes[number])[]);
 | 
			
		||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]);
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class NotificationEntityService implements OnModuleInit {
 | 
			
		||||
| 
						 | 
				
			
			@ -169,6 +169,9 @@ export class NotificationEntityService implements OnModuleInit {
 | 
			
		|||
				exportedEntity: notification.exportedEntity,
 | 
			
		||||
				fileId: notification.fileId,
 | 
			
		||||
			} : {}),
 | 
			
		||||
			...(notification.type === 'scheduledNoteFailed' ? {
 | 
			
		||||
				reason: notification.reason,
 | 
			
		||||
			} : {}),
 | 
			
		||||
			...(notification.type === 'app' ? {
 | 
			
		||||
				body: notification.customBody,
 | 
			
		||||
				header: notification.customHeader,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,5 +86,6 @@ export const DI = {
 | 
			
		|||
	noteEditRepository: Symbol('noteEditRepository'),
 | 
			
		||||
	bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
 | 
			
		||||
	reversiGamesRepository: Symbol('reversiGamesRepository'),
 | 
			
		||||
	noteScheduleRepository: Symbol('noteScheduleRepository'),
 | 
			
		||||
	//#endregion
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										58
									
								
								packages/backend/src/models/NoteSchedule.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								packages/backend/src/models/NoteSchedule.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiChannel } from './Channel.js';
 | 
			
		||||
import type { MiDriveFile } from './DriveFile.js';
 | 
			
		||||
 | 
			
		||||
type MinimumUser = {
 | 
			
		||||
	id: MiUser['id'];
 | 
			
		||||
	host: MiUser['host'];
 | 
			
		||||
	username: MiUser['username'];
 | 
			
		||||
	uri: MiUser['uri'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MiScheduleNoteType={
 | 
			
		||||
	visibility: 'public' | 'home' | 'followers' | 'specified';
 | 
			
		||||
	visibleUsers: MinimumUser[];
 | 
			
		||||
	channel?: MiChannel['id'];
 | 
			
		||||
	poll: {
 | 
			
		||||
		multiple: boolean;
 | 
			
		||||
		choices: string[];
 | 
			
		||||
		/** Date.toISOString() */
 | 
			
		||||
		expiresAt: string | null
 | 
			
		||||
	} | undefined;
 | 
			
		||||
	renote?: MiNote['id'];
 | 
			
		||||
	localOnly: boolean;
 | 
			
		||||
	cw?: string | null;
 | 
			
		||||
	reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
 | 
			
		||||
	files: MiDriveFile['id'][];
 | 
			
		||||
	text?: string | null;
 | 
			
		||||
	reply?: MiNote['id'];
 | 
			
		||||
	apMentions?: MinimumUser[] | null;
 | 
			
		||||
	apHashtags?: string[] | null;
 | 
			
		||||
	apEmojis?: string[] | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Entity('note_schedule')
 | 
			
		||||
export class MiNoteSchedule {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Column('jsonb')
 | 
			
		||||
	public note: MiScheduleNoteType;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 260,
 | 
			
		||||
	})
 | 
			
		||||
	public userId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@Column('timestamp with time zone')
 | 
			
		||||
	public scheduledAt: Date;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +122,16 @@ export type MiNotification = {
 | 
			
		|||
	createdAt: string;
 | 
			
		||||
	notifierId: MiUser['id'];
 | 
			
		||||
	noteId: MiNote['id'];
 | 
			
		||||
} | {
 | 
			
		||||
	type: 'scheduledNoteFailed';
 | 
			
		||||
	id: string;
 | 
			
		||||
	createdAt: string;
 | 
			
		||||
	reason: string;
 | 
			
		||||
} | {
 | 
			
		||||
	type: 'scheduledNotePosted';
 | 
			
		||||
	id: string;
 | 
			
		||||
	createdAt: string;
 | 
			
		||||
	noteId: MiNote['id'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type MiGroupedNotification = MiNotification | {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,7 @@ import {
 | 
			
		|||
	MiNote,
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteSchedule,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
| 
						 | 
				
			
			@ -509,6 +510,12 @@ const $reversiGamesRepository: Provider = {
 | 
			
		|||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $noteScheduleRepository: Provider = {
 | 
			
		||||
	provide: DI.noteScheduleRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiNoteSchedule).extend(miRepository as MiRepository<MiNoteSchedule>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [],
 | 
			
		||||
	providers: [
 | 
			
		||||
| 
						 | 
				
			
			@ -583,6 +590,7 @@ const $reversiGamesRepository: Provider = {
 | 
			
		|||
		$noteEditRepository,
 | 
			
		||||
		$bubbleGameRecordsRepository,
 | 
			
		||||
		$reversiGamesRepository,
 | 
			
		||||
		$noteScheduleRepository,
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		$usersRepository,
 | 
			
		||||
| 
						 | 
				
			
			@ -656,6 +664,7 @@ const $reversiGamesRepository: Provider = {
 | 
			
		|||
		$noteEditRepository,
 | 
			
		||||
		$bubbleGameRecordsRepository,
 | 
			
		||||
		$reversiGamesRepository,
 | 
			
		||||
		$noteScheduleRepository,
 | 
			
		||||
	],
 | 
			
		||||
})
 | 
			
		||||
export class RepositoryModule {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,6 +81,7 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js';
 | 
			
		|||
import { NoteEdit } from '@/models/NoteEdit.js';
 | 
			
		||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
 | 
			
		||||
import { MiReversiGame } from '@/models/ReversiGame.js';
 | 
			
		||||
import { MiNoteSchedule } from '@/models/NoteSchedule.js';
 | 
			
		||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
 | 
			
		||||
 | 
			
		||||
export interface MiRepository<T extends ObjectLiteral> {
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +161,7 @@ export {
 | 
			
		|||
	MiNote,
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteSchedule,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
| 
						 | 
				
			
			@ -271,3 +273,4 @@ export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMem
 | 
			
		|||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
 | 
			
		||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
 | 
			
		||||
export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>;
 | 
			
		||||
export type NoteScheduleRepository = Repository<MiNoteSchedule>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -369,6 +369,45 @@ export const packedNotificationSchema = {
 | 
			
		|||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		properties: {
 | 
			
		||||
			...baseSchema.properties,
 | 
			
		||||
			type: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
				enum: ['scheduledNoteFailed'],
 | 
			
		||||
			},
 | 
			
		||||
			reason: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		properties: {
 | 
			
		||||
			...baseSchema.properties,
 | 
			
		||||
			type: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
				enum: ['scheduledNotePosted'],
 | 
			
		||||
			},
 | 
			
		||||
			user: {
 | 
			
		||||
				type: 'object',
 | 
			
		||||
				ref: 'UserLite',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
			userId: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
				format: 'id',
 | 
			
		||||
			},
 | 
			
		||||
			note: {
 | 
			
		||||
				type: 'object',
 | 
			
		||||
				ref: 'Note',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		properties: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -296,6 +296,10 @@ export const packedRolePoliciesSchema = {
 | 
			
		|||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		scheduleNoteMax: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,6 +79,7 @@ import { MiUserMemo } from '@/models/UserMemo.js';
 | 
			
		|||
import { NoteEdit } from '@/models/NoteEdit.js';
 | 
			
		||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
 | 
			
		||||
import { MiReversiGame } from '@/models/ReversiGame.js';
 | 
			
		||||
import { MiNoteSchedule } from '@/models/NoteSchedule.js';
 | 
			
		||||
 | 
			
		||||
import { Config } from '@/config.js';
 | 
			
		||||
import MisskeyLogger from '@/logger.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -158,6 +159,7 @@ export const entities = [
 | 
			
		|||
	MiNote,
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteSchedule,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,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 { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +86,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 | 
			
		|||
		InboxProcessorService,
 | 
			
		||||
		AggregateRetentionProcessorService,
 | 
			
		||||
		QueueProcessorService,
 | 
			
		||||
		ScheduleNotePostProcessorService,
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		QueueProcessorService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
 | 
			
		|||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
 | 
			
		||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
 | 
			
		||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
 | 
			
		||||
import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js';
 | 
			
		||||
import { QueueLoggerService } from './QueueLoggerService.js';
 | 
			
		||||
import { QUEUE, baseQueueOptions } from './const.js';
 | 
			
		||||
import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +87,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
	private relationshipQueueWorker: Bull.Worker;
 | 
			
		||||
	private objectStorageQueueWorker: Bull.Worker;
 | 
			
		||||
	private endedPollNotificationQueueWorker: Bull.Worker;
 | 
			
		||||
	private schedulerNotePostQueueWorker: Bull.Worker;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +128,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
 | 
			
		||||
		private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
 | 
			
		||||
		private cleanProcessorService: CleanProcessorService,
 | 
			
		||||
		private scheduleNotePostProcessorService: ScheduleNotePostProcessorService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -530,6 +533,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
			});
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		//#region schedule note post
 | 
			
		||||
		{
 | 
			
		||||
			this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), {
 | 
			
		||||
				...baseQueueOptions(this.config, QUEUE.SCHEDULE_NOTE_POST),
 | 
			
		||||
				autorun: false,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			@ -544,6 +556,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
			this.relationshipQueueWorker.run(),
 | 
			
		||||
			this.objectStorageQueueWorker.run(),
 | 
			
		||||
			this.endedPollNotificationQueueWorker.run(),
 | 
			
		||||
			this.schedulerNotePostQueueWorker.run(),
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -559,6 +572,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
			this.relationshipQueueWorker.close(),
 | 
			
		||||
			this.objectStorageQueueWorker.close(),
 | 
			
		||||
			this.endedPollNotificationQueueWorker.close(),
 | 
			
		||||
			this.schedulerNotePostQueueWorker.close(),
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ export const QUEUE = {
 | 
			
		|||
	OBJECT_STORAGE: 'objectStorage',
 | 
			
		||||
	USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
 | 
			
		||||
	SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
 | 
			
		||||
	SCHEDULE_NOTE_POST: 'scheduleNotePost',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
 | 
			
		||||
import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import type { MiScheduleNoteType } from '@/models/NoteSchedule.js';
 | 
			
		||||
import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
			
		||||
import type * as Bull from 'bullmq';
 | 
			
		||||
import type { ScheduleNotePostJobData } from '../types.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ScheduleNotePostProcessorService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.noteScheduleRepository)
 | 
			
		||||
		private noteScheduleRepository: NoteScheduleRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
		@Inject(DI.channelsRepository)
 | 
			
		||||
		private channelsRepository: ChannelsRepository,
 | 
			
		||||
 | 
			
		||||
		private noteCreateService: NoteCreateService,
 | 
			
		||||
		private queueLoggerService: QueueLoggerService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async isValidNoteSchedule(note: MiScheduleNoteType, id: string): Promise<boolean> {
 | 
			
		||||
		const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined;
 | 
			
		||||
		const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined;
 | 
			
		||||
		const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined;
 | 
			
		||||
		if (note.reply && !reply) {
 | 
			
		||||
			this.logger.warn('Schedule Note Failed Reason: parent note to reply does not exist');
 | 
			
		||||
			this.notificationService.createNotification(id, 'scheduledNoteFailed', {
 | 
			
		||||
				reason: 'Replied to note on your scheduled note no longer exists',
 | 
			
		||||
			});
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		if (note.renote && !renote) {
 | 
			
		||||
			this.logger.warn('Schedule Note Failed Reason: attached quote note no longer exists');
 | 
			
		||||
			this.notificationService.createNotification(id, 'scheduledNoteFailed', {
 | 
			
		||||
				reason: 'A quoted note from one of your scheduled notes no longer exists',
 | 
			
		||||
			});
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		if (note.channel && !channel) {
 | 
			
		||||
			this.logger.warn('Schedule Note Failed Reason: Channel does not exist');
 | 
			
		||||
			this.notificationService.createNotification(id, 'scheduledNoteFailed', {
 | 
			
		||||
				reason: 'An attached channel on your scheduled note no longer exists',
 | 
			
		||||
			});
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> {
 | 
			
		||||
		this.noteScheduleRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => {
 | 
			
		||||
			if (!data) {
 | 
			
		||||
				this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`);
 | 
			
		||||
			} else {
 | 
			
		||||
				const me = await this.usersRepository.findOneBy({ id: data.userId });
 | 
			
		||||
				const note = data.note;
 | 
			
		||||
				const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined;
 | 
			
		||||
				const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined;
 | 
			
		||||
				const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined;
 | 
			
		||||
 | 
			
		||||
				let files: MiDriveFile[] = [];
 | 
			
		||||
				const fileIds = note.files;
 | 
			
		||||
 | 
			
		||||
				if (fileIds.length > 0 && me) {
 | 
			
		||||
					files = await this.driveFilesRepository.createQueryBuilder('file')
 | 
			
		||||
						.where('file.userId = :userId AND file.id IN (:...fileIds)', {
 | 
			
		||||
							userId: me.id,
 | 
			
		||||
							fileIds,
 | 
			
		||||
						})
 | 
			
		||||
						.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
 | 
			
		||||
						.setParameters({ fileIds })
 | 
			
		||||
						.getMany();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (!data.userId || !me) {
 | 
			
		||||
					this.logger.warn('Schedule Note Failed Reason: User Not Found');
 | 
			
		||||
					await this.noteScheduleRepository.remove(data);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (!await this.isValidNoteSchedule(note, me.id)) {
 | 
			
		||||
					await this.noteScheduleRepository.remove(data);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (note.files.length !== files.length) {
 | 
			
		||||
					this.logger.warn('Schedule Note Failed Reason: files are missing in the user\'s drive');
 | 
			
		||||
					this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
 | 
			
		||||
						reason: 'Some attached files on your scheduled note no longer exist',
 | 
			
		||||
					});
 | 
			
		||||
					await this.noteScheduleRepository.remove(data);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const createdNote = await this.noteCreateService.create(me, {
 | 
			
		||||
					...note,
 | 
			
		||||
					createdAt: new Date(),
 | 
			
		||||
					files,
 | 
			
		||||
					poll: note.poll ? {
 | 
			
		||||
						choices: note.poll.choices,
 | 
			
		||||
						multiple: note.poll.multiple,
 | 
			
		||||
						expiresAt: note.poll.expiresAt ? new Date(note.poll.expiresAt) : null,
 | 
			
		||||
					} : undefined,
 | 
			
		||||
					reply,
 | 
			
		||||
					renote,
 | 
			
		||||
					channel,
 | 
			
		||||
				}).catch(async (err: IdentifiableError) => {
 | 
			
		||||
					this.notificationService.createNotification(me.id, 'scheduledNoteFailed', {
 | 
			
		||||
						reason: err.message,
 | 
			
		||||
					});
 | 
			
		||||
					await this.noteScheduleRepository.remove(data);
 | 
			
		||||
					throw this.logger.error(`Schedule Note Failed Reason: ${err.message}`);
 | 
			
		||||
				});
 | 
			
		||||
				await this.noteScheduleRepository.remove(data);
 | 
			
		||||
				this.notificationService.createNotification(me.id, 'scheduledNotePosted', {
 | 
			
		||||
					noteId: createdNote.id,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -155,3 +155,7 @@ export type UserWebhookDeliverJobData = {
 | 
			
		|||
export type ThinUser = {
 | 
			
		||||
	id: MiUser['id'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ScheduleNotePostJobData = {
 | 
			
		||||
	scheduleNoteId: MiNote['id'];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -311,6 +311,9 @@ import * as ep___notes_renotes from './endpoints/notes/renotes.js';
 | 
			
		|||
import * as ep___notes_replies from './endpoints/notes/replies.js';
 | 
			
		||||
import * as ep___notes_edit from './endpoints/notes/edit.js';
 | 
			
		||||
import * as ep___notes_versions from './endpoints/notes/versions.js';
 | 
			
		||||
import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
 | 
			
		||||
import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
 | 
			
		||||
import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
 | 
			
		||||
import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js';
 | 
			
		||||
import * as ep___notes_search from './endpoints/notes/search.js';
 | 
			
		||||
import * as ep___notes_show from './endpoints/notes/show.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -711,6 +714,9 @@ const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete'
 | 
			
		|||
const $notes_like: Provider = { provide: 'ep:notes/like', useClass: ep___notes_like.default };
 | 
			
		||||
const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default };
 | 
			
		||||
const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default };
 | 
			
		||||
const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default };
 | 
			
		||||
const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default };
 | 
			
		||||
const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default };
 | 
			
		||||
const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default };
 | 
			
		||||
const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default };
 | 
			
		||||
const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default };
 | 
			
		||||
| 
						 | 
				
			
			@ -1117,6 +1123,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		|||
		$notes_like,
 | 
			
		||||
		$notes_renotes,
 | 
			
		||||
		$notes_replies,
 | 
			
		||||
		$notes_schedule_create,
 | 
			
		||||
		$notes_schedule_delete,
 | 
			
		||||
		$notes_schedule_list,
 | 
			
		||||
		$notes_searchByTag,
 | 
			
		||||
		$notes_search,
 | 
			
		||||
		$notes_show,
 | 
			
		||||
| 
						 | 
				
			
			@ -1516,6 +1525,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 | 
			
		|||
		$notes_like,
 | 
			
		||||
		$notes_renotes,
 | 
			
		||||
		$notes_replies,
 | 
			
		||||
		$notes_schedule_create,
 | 
			
		||||
		$notes_schedule_delete,
 | 
			
		||||
		$notes_schedule_list,
 | 
			
		||||
		$notes_searchByTag,
 | 
			
		||||
		$notes_search,
 | 
			
		||||
		$notes_show,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -316,6 +316,9 @@ import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete
 | 
			
		|||
import * as ep___notes_like from './endpoints/notes/like.js';
 | 
			
		||||
import * as ep___notes_renotes from './endpoints/notes/renotes.js';
 | 
			
		||||
import * as ep___notes_replies from './endpoints/notes/replies.js';
 | 
			
		||||
import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
 | 
			
		||||
import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
 | 
			
		||||
import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
 | 
			
		||||
import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js';
 | 
			
		||||
import * as ep___notes_search from './endpoints/notes/search.js';
 | 
			
		||||
import * as ep___notes_show from './endpoints/notes/show.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -716,6 +719,9 @@ const eps = [
 | 
			
		|||
	['notes/like', ep___notes_like],
 | 
			
		||||
	['notes/renotes', ep___notes_renotes],
 | 
			
		||||
	['notes/replies', ep___notes_replies],
 | 
			
		||||
	['notes/schedule/create', ep___notes_schedule_create],
 | 
			
		||||
	['notes/schedule/delete', ep___notes_schedule_delete],
 | 
			
		||||
	['notes/schedule/list', ep___notes_schedule_list],
 | 
			
		||||
	['notes/search-by-tag', ep___notes_searchByTag],
 | 
			
		||||
	['notes/search', ep___notes_search],
 | 
			
		||||
	['notes/show', ep___notes_show],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
 | 
			
		||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduleNotePostQueue } from '@/core/QueueModule.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
			
		||||
		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const deliverJobCounts = await this.deliverQueue.getJobCounts();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,370 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { isPureRenote } from '@/misc/is-renote.js';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import type {
 | 
			
		||||
	UsersRepository,
 | 
			
		||||
	NotesRepository,
 | 
			
		||||
	BlockingsRepository,
 | 
			
		||||
	DriveFilesRepository,
 | 
			
		||||
	ChannelsRepository,
 | 
			
		||||
	NoteScheduleRepository,
 | 
			
		||||
} from '@/models/_.js';
 | 
			
		||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import type { MiChannel } from '@/models/Channel.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { MiScheduleNoteType } from '@/models/NoteSchedule.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['notes'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 300,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	kind: 'write:notes-schedule',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		scheduleNoteMax: {
 | 
			
		||||
			message: 'Schedule note max.',
 | 
			
		||||
			code: 'SCHEDULE_NOTE_MAX',
 | 
			
		||||
			id: '168707c3-e7da-4031-989e-f42aa3a274b2',
 | 
			
		||||
		},
 | 
			
		||||
		noSuchRenoteTarget: {
 | 
			
		||||
			message: 'No such renote target.',
 | 
			
		||||
			code: 'NO_SUCH_RENOTE_TARGET',
 | 
			
		||||
			id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotReRenote: {
 | 
			
		||||
			message: 'You can not Renote a pure Renote.',
 | 
			
		||||
			code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
 | 
			
		||||
			id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotRenoteDueToVisibility: {
 | 
			
		||||
			message: 'You can not Renote due to target visibility.',
 | 
			
		||||
			code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
 | 
			
		||||
			id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchReplyTarget: {
 | 
			
		||||
			message: 'No such reply target.',
 | 
			
		||||
			code: 'NO_SUCH_REPLY_TARGET',
 | 
			
		||||
			id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotReplyToPureRenote: {
 | 
			
		||||
			message: 'You can not reply to a pure Renote.',
 | 
			
		||||
			code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
 | 
			
		||||
			id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotCreateAlreadyExpiredPoll: {
 | 
			
		||||
			message: 'Poll is already expired.',
 | 
			
		||||
			code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
 | 
			
		||||
			id: '04da457d-b083-4055-9082-955525eda5a5',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotCreateAlreadyExpiredSchedule: {
 | 
			
		||||
			message: 'Schedule is already expired.',
 | 
			
		||||
			code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE',
 | 
			
		||||
			id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchChannel: {
 | 
			
		||||
			message: 'No such channel.',
 | 
			
		||||
			code: 'NO_SUCH_CHANNEL',
 | 
			
		||||
			id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
 | 
			
		||||
		},
 | 
			
		||||
		noSuchSchedule: {
 | 
			
		||||
			message: 'No such schedule.',
 | 
			
		||||
			code: 'NO_SUCH_SCHEDULE',
 | 
			
		||||
			id: '44dee229-8da1-4a61-856d-e3a4bbc12032',
 | 
			
		||||
		},
 | 
			
		||||
		youHaveBeenBlocked: {
 | 
			
		||||
			message: 'You have been blocked by this user.',
 | 
			
		||||
			code: 'YOU_HAVE_BEEN_BLOCKED',
 | 
			
		||||
			id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchFile: {
 | 
			
		||||
			message: 'Some files are not found.',
 | 
			
		||||
			code: 'NO_SUCH_FILE',
 | 
			
		||||
			id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotRenoteOutsideOfChannel: {
 | 
			
		||||
			message: 'Cannot renote outside of channel.',
 | 
			
		||||
			code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
 | 
			
		||||
			id: '33510210-8452-094c-6227-4a6c05d99f00',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
 | 
			
		||||
		visibleUserIds: { type: 'array', uniqueItems: true, items: {
 | 
			
		||||
			type: 'string', format: 'misskey:id',
 | 
			
		||||
		} },
 | 
			
		||||
		cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
 | 
			
		||||
		reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
 | 
			
		||||
		noExtractMentions: { type: 'boolean', default: false },
 | 
			
		||||
		noExtractHashtags: { type: 'boolean', default: false },
 | 
			
		||||
		noExtractEmojis: { type: 'boolean', default: false },
 | 
			
		||||
		replyId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		renoteId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
 | 
			
		||||
		// anyOf内にバリデーションを書いても最初の一つしかチェックされない
 | 
			
		||||
		// See https://github.com/misskey-dev/misskey/pull/10082
 | 
			
		||||
		text: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			minLength: 1,
 | 
			
		||||
			nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		fileIds: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			uniqueItems: true,
 | 
			
		||||
			minItems: 1,
 | 
			
		||||
			maxItems: 16,
 | 
			
		||||
			items: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		},
 | 
			
		||||
		mediaIds: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			uniqueItems: true,
 | 
			
		||||
			minItems: 1,
 | 
			
		||||
			maxItems: 16,
 | 
			
		||||
			items: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		},
 | 
			
		||||
		poll: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			nullable: true,
 | 
			
		||||
			properties: {
 | 
			
		||||
				choices: {
 | 
			
		||||
					type: 'array',
 | 
			
		||||
					uniqueItems: true,
 | 
			
		||||
					minItems: 2,
 | 
			
		||||
					maxItems: 10,
 | 
			
		||||
					items: { type: 'string', minLength: 1, maxLength: 50 },
 | 
			
		||||
				},
 | 
			
		||||
				multiple: { type: 'boolean' },
 | 
			
		||||
				expiresAt: { type: 'integer', nullable: true },
 | 
			
		||||
				expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
 | 
			
		||||
			},
 | 
			
		||||
			required: ['choices'],
 | 
			
		||||
		},
 | 
			
		||||
		scheduleNote: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			nullable: false,
 | 
			
		||||
			properties: {
 | 
			
		||||
				scheduledAt: { type: 'integer', nullable: false },
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	// (re)note with text, files and poll are optional
 | 
			
		||||
	anyOf: [
 | 
			
		||||
		{ required: ['text'] },
 | 
			
		||||
		{ required: ['renoteId'] },
 | 
			
		||||
		{ required: ['fileIds'] },
 | 
			
		||||
		{ required: ['mediaIds'] },
 | 
			
		||||
		{ required: ['poll'] },
 | 
			
		||||
	],
 | 
			
		||||
	required: ['scheduleNote'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.noteScheduleRepository)
 | 
			
		||||
		private noteScheduleRepository: NoteScheduleRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.blockingsRepository)
 | 
			
		||||
		private blockingsRepository: BlockingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.channelsRepository)
 | 
			
		||||
		private channelsRepository: ChannelsRepository,
 | 
			
		||||
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const scheduleNoteCount = await this.noteScheduleRepository.countBy({ userId: me.id });
 | 
			
		||||
			const scheduleNoteMax = (await this.roleService.getUserPolicies(me.id)).scheduleNoteMax;
 | 
			
		||||
			if (scheduleNoteCount >= scheduleNoteMax) {
 | 
			
		||||
				throw new ApiError(meta.errors.scheduleNoteMax);
 | 
			
		||||
			}
 | 
			
		||||
			let visibleUsers: MiUser[] = [];
 | 
			
		||||
			if (ps.visibleUserIds) {
 | 
			
		||||
				visibleUsers = await this.usersRepository.findBy({
 | 
			
		||||
					id: In(ps.visibleUserIds),
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let files: MiDriveFile[] = [];
 | 
			
		||||
			const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
 | 
			
		||||
			if (fileIds != null) {
 | 
			
		||||
				files = await this.driveFilesRepository.createQueryBuilder('file')
 | 
			
		||||
					.where('file.userId = :userId AND file.id IN (:...fileIds)', {
 | 
			
		||||
						userId: me.id,
 | 
			
		||||
						fileIds,
 | 
			
		||||
					})
 | 
			
		||||
					.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
 | 
			
		||||
					.setParameters({ fileIds })
 | 
			
		||||
					.getMany();
 | 
			
		||||
 | 
			
		||||
				if (files.length !== fileIds.length) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchFile);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let renote: MiNote | null = null;
 | 
			
		||||
			if (ps.renoteId != null) {
 | 
			
		||||
				// Fetch renote to note
 | 
			
		||||
				renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
 | 
			
		||||
 | 
			
		||||
				if (renote == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchRenoteTarget);
 | 
			
		||||
				} else if (isPureRenote(renote)) {
 | 
			
		||||
					throw new ApiError(meta.errors.cannotReRenote);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Check blocking
 | 
			
		||||
				if (renote.userId !== me.id) {
 | 
			
		||||
					const blockExist = await this.blockingsRepository.exist({
 | 
			
		||||
						where: {
 | 
			
		||||
							blockerId: renote.userId,
 | 
			
		||||
							blockeeId: me.id,
 | 
			
		||||
						},
 | 
			
		||||
					});
 | 
			
		||||
					if (blockExist) {
 | 
			
		||||
						throw new ApiError(meta.errors.youHaveBeenBlocked);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (renote.visibility === 'followers' && renote.userId !== me.id) {
 | 
			
		||||
					// 他人のfollowers noteはreject
 | 
			
		||||
					throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
 | 
			
		||||
				} else if (renote.visibility === 'specified') {
 | 
			
		||||
					// specified / direct noteはreject
 | 
			
		||||
					throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let reply: MiNote | null = null;
 | 
			
		||||
			if (ps.replyId != null) {
 | 
			
		||||
				// Fetch reply
 | 
			
		||||
				reply = await this.notesRepository.findOneBy({ id: ps.replyId });
 | 
			
		||||
 | 
			
		||||
				if (reply == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchReplyTarget);
 | 
			
		||||
				} else if (isPureRenote(reply)) {
 | 
			
		||||
					throw new ApiError(meta.errors.cannotReplyToPureRenote);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Check blocking
 | 
			
		||||
				if (reply.userId !== me.id) {
 | 
			
		||||
					const blockExist = await this.blockingsRepository.exists({
 | 
			
		||||
						where: {
 | 
			
		||||
							blockerId: reply.userId,
 | 
			
		||||
							blockeeId: me.id,
 | 
			
		||||
						},
 | 
			
		||||
					});
 | 
			
		||||
					if (blockExist) {
 | 
			
		||||
						throw new ApiError(meta.errors.youHaveBeenBlocked);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.poll) {
 | 
			
		||||
				let scheduleNote_scheduledAt = Date.now();
 | 
			
		||||
				if (typeof ps.scheduleNote.scheduledAt === 'number') {
 | 
			
		||||
					scheduleNote_scheduledAt = ps.scheduleNote.scheduledAt;
 | 
			
		||||
				}
 | 
			
		||||
				if (typeof ps.poll.expiresAt === 'number') {
 | 
			
		||||
					if (ps.poll.expiresAt < scheduleNote_scheduledAt) {
 | 
			
		||||
						throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
 | 
			
		||||
					}
 | 
			
		||||
				} else if (typeof ps.poll.expiredAfter === 'number') {
 | 
			
		||||
					ps.poll.expiresAt = scheduleNote_scheduledAt + ps.poll.expiredAfter;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (typeof ps.scheduleNote.scheduledAt === 'number') {
 | 
			
		||||
				if (ps.scheduleNote.scheduledAt < Date.now()) {
 | 
			
		||||
					throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule);
 | 
			
		||||
			}
 | 
			
		||||
			const note: MiScheduleNoteType = {
 | 
			
		||||
				files: files.map(f => f.id),
 | 
			
		||||
				poll: ps.poll ? {
 | 
			
		||||
					choices: ps.poll.choices,
 | 
			
		||||
					multiple: ps.poll.multiple ?? false,
 | 
			
		||||
					expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt).toISOString() : null,
 | 
			
		||||
				} : undefined,
 | 
			
		||||
				text: ps.text ?? undefined,
 | 
			
		||||
				reply: reply?.id,
 | 
			
		||||
				renote: renote?.id,
 | 
			
		||||
				cw: ps.cw,
 | 
			
		||||
				localOnly: false,
 | 
			
		||||
				reactionAcceptance: ps.reactionAcceptance,
 | 
			
		||||
				visibility: ps.visibility,
 | 
			
		||||
				visibleUsers,
 | 
			
		||||
				apMentions: ps.noExtractMentions ? [] : undefined,
 | 
			
		||||
				apHashtags: ps.noExtractHashtags ? [] : undefined,
 | 
			
		||||
				apEmojis: ps.noExtractEmojis ? [] : undefined,
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			if (ps.scheduleNote.scheduledAt) {
 | 
			
		||||
				me.token = null;
 | 
			
		||||
				const noteId = this.idService.gen(new Date().getTime());
 | 
			
		||||
				await this.noteScheduleRepository.insert({
 | 
			
		||||
					id: noteId,
 | 
			
		||||
					note: note,
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
					scheduledAt: new Date(ps.scheduleNote.scheduledAt),
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				const delay = new Date(ps.scheduleNote.scheduledAt).getTime() - Date.now();
 | 
			
		||||
				await this.queueService.ScheduleNotePostQueue.add(String(delay), {
 | 
			
		||||
					scheduleNoteId: noteId,
 | 
			
		||||
				}, {
 | 
			
		||||
					delay,
 | 
			
		||||
					removeOnComplete: true,
 | 
			
		||||
					jobId: `schedNote:${noteId}`,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return '';
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { NoteScheduleRepository } from '@/models/_.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['notes'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	kind: 'write:notes-schedule',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 300,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchNote: {
 | 
			
		||||
			message: 'No such note.',
 | 
			
		||||
			code: 'NO_SUCH_NOTE',
 | 
			
		||||
			id: 'a58056ba-8ba1-4323-8ebf-e0b585bc244f',
 | 
			
		||||
		},
 | 
			
		||||
		permissionDenied: {
 | 
			
		||||
			message: 'Permission denied.',
 | 
			
		||||
			code: 'PERMISSION_DENIED',
 | 
			
		||||
			id: 'c0da2fed-8f61-4c47-a41d-431992607b5c',
 | 
			
		||||
			httpStatusCode: 403,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		noteId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['noteId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.noteScheduleRepository)
 | 
			
		||||
		private noteScheduleRepository: NoteScheduleRepository,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const note = await this.noteScheduleRepository.findOneBy({ id: ps.noteId });
 | 
			
		||||
			if (note === null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchNote);
 | 
			
		||||
			}
 | 
			
		||||
			if (note.userId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.permissionDenied);
 | 
			
		||||
			}
 | 
			
		||||
			await this.noteScheduleRepository.delete({ id: ps.noteId });
 | 
			
		||||
			await this.queueService.ScheduleNotePostQueue.remove(`schedNote:${ps.noteId}`);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										131
									
								
								packages/backend/src/server/api/endpoints/notes/schedule/list.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								packages/backend/src/server/api/endpoints/notes/schedule/list.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,131 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { noteVisibilities } from '@/types.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['notes'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	kind: 'read:notes-schedule',
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			properties: {
 | 
			
		||||
				id: { type: 'string', format: 'misskey:id', optional: false, nullable: false },
 | 
			
		||||
				note: {
 | 
			
		||||
					type: 'object',
 | 
			
		||||
					optional: false, nullable: false,
 | 
			
		||||
					properties: {
 | 
			
		||||
						createdAt: { type: 'string', optional: false, nullable: false },
 | 
			
		||||
						text: { type: 'string', optional: true, nullable: false },
 | 
			
		||||
						cw: { type: 'string', optional: true, nullable: true },
 | 
			
		||||
						fileIds: { type: 'array', optional: false, nullable: false, items: { type: 'string', format: 'misskey:id', optional: false, nullable: false } },
 | 
			
		||||
						visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], optional: false, nullable: false },
 | 
			
		||||
						visibleUsers: {
 | 
			
		||||
							type: 'array', optional: false, nullable: false, items: {
 | 
			
		||||
								type: 'object',
 | 
			
		||||
								optional: false, nullable: false,
 | 
			
		||||
								ref: 'UserLite',
 | 
			
		||||
							},
 | 
			
		||||
						},
 | 
			
		||||
						user: {
 | 
			
		||||
							type: 'object',
 | 
			
		||||
							optional: false, nullable: false,
 | 
			
		||||
							ref: 'User',
 | 
			
		||||
						},
 | 
			
		||||
						reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
 | 
			
		||||
						isSchedule: { type: 'boolean', optional: false, nullable: false },
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				userId: { type: 'string', optional: false, nullable: false },
 | 
			
		||||
				scheduledAt: { type: 'string', optional: false, nullable: false },
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 300,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.noteScheduleRepository)
 | 
			
		||||
		private noteScheduleRepository: NoteScheduleRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const query = this.queryService.makePaginationQuery(this.noteScheduleRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 | 
			
		||||
				.andWhere('note.userId = :userId', { userId: me.id });
 | 
			
		||||
			const scheduleNotes = await query.limit(ps.limit).getMany();
 | 
			
		||||
			const user = await this.userEntityService.pack(me, me);
 | 
			
		||||
			const scheduleNotesPack: {
 | 
			
		||||
				id: string;
 | 
			
		||||
				note: {
 | 
			
		||||
					text?: string;
 | 
			
		||||
					cw?: string|null;
 | 
			
		||||
					fileIds: string[];
 | 
			
		||||
					visibility: typeof noteVisibilities[number];
 | 
			
		||||
					visibleUsers: Packed<'UserLite'>[];
 | 
			
		||||
					reactionAcceptance: MiNote['reactionAcceptance'];
 | 
			
		||||
					user: Packed<'User'>;
 | 
			
		||||
					createdAt: string;
 | 
			
		||||
					isSchedule: boolean;
 | 
			
		||||
				};
 | 
			
		||||
				userId: string;
 | 
			
		||||
				scheduledAt: string;
 | 
			
		||||
			}[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
 | 
			
		||||
				return {
 | 
			
		||||
					...item,
 | 
			
		||||
					scheduledAt: item.scheduledAt.toISOString(),
 | 
			
		||||
					note: {
 | 
			
		||||
						...item.note,
 | 
			
		||||
						text: item.note.text ?? '',
 | 
			
		||||
						user: user,
 | 
			
		||||
						visibility: item.note.visibility ?? 'public',
 | 
			
		||||
						reactionAcceptance: item.note.reactionAcceptance ?? null,
 | 
			
		||||
						visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
 | 
			
		||||
						fileIds: item.note.files ? item.note.files : [],
 | 
			
		||||
						files: await this.driveFileEntityService.packManyByIds(item.note.files),
 | 
			
		||||
						createdAt: item.scheduledAt.toISOString(),
 | 
			
		||||
						isSchedule: true,
 | 
			
		||||
						id: item.id,
 | 
			
		||||
					},
 | 
			
		||||
				};
 | 
			
		||||
			}));
 | 
			
		||||
 | 
			
		||||
			return scheduleNotesPack;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ import type {
 | 
			
		|||
	SystemQueue,
 | 
			
		||||
	UserWebhookDeliverQueue,
 | 
			
		||||
	SystemWebhookDeliverQueue,
 | 
			
		||||
	ScheduleNotePostQueue,
 | 
			
		||||
} from '@/core/QueueModule.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +125,7 @@ export class ClientServerService {
 | 
			
		|||
		@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
 | 
			
		||||
		@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
 | 
			
		||||
		@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
 | 
			
		||||
	) {
 | 
			
		||||
		//this.createServer = this.createServer.bind(this);
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -254,6 +256,7 @@ export class ClientServerService {
 | 
			
		|||
				this.objectStorageQueue,
 | 
			
		||||
				this.userWebhookDeliverQueue,
 | 
			
		||||
				this.systemWebhookDeliverQueue,
 | 
			
		||||
				this.scheduleNotePostQueue,
 | 
			
		||||
			].map(q => new BullMQAdapter(q)),
 | 
			
		||||
			serverAdapter: bullBoardServerAdapter,
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,8 @@ export const notificationTypes = [
 | 
			
		|||
	'roleAssigned',
 | 
			
		||||
	'achievementEarned',
 | 
			
		||||
	'exportCompleted',
 | 
			
		||||
	'scheduledNoteFailed',
 | 
			
		||||
	'scheduledNotePosted',
 | 
			
		||||
	'app',
 | 
			
		||||
	'test',
 | 
			
		||||
] as const;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,6 +131,8 @@ export const notificationTypes = [
 | 
			
		|||
	'test',
 | 
			
		||||
	'app',
 | 
			
		||||
	'edited',
 | 
			
		||||
	'scheduledNoteFailed',
 | 
			
		||||
	'scheduledNotePosted',
 | 
			
		||||
] as const;
 | 
			
		||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +142,7 @@ export const ROLE_POLICIES = [
 | 
			
		|||
	'btlAvailable',
 | 
			
		||||
	'canPublicNote',
 | 
			
		||||
	'canImportNotes',
 | 
			
		||||
	'scheduleNoteMax',
 | 
			
		||||
	'mentionLimit',
 | 
			
		||||
	'canInvite',
 | 
			
		||||
	'inviteLimit',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,7 +50,10 @@ import { popupMenu } from '@/os.js';
 | 
			
		|||
import { defaultStore } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	note: Misskey.entities.Note & {
 | 
			
		||||
		isSchedule?: boolean
 | 
			
		||||
	};
 | 
			
		||||
	scheduled?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const menuVersionsButton = shallowRef<HTMLElement>();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
<div v-if="!isDeleted" :class="$style.root">
 | 
			
		||||
	<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			</p>
 | 
			
		||||
			<div v-show="note.cw == null || showContent">
 | 
			
		||||
				<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
				<div v-if="note.isSchedule" style="margin-top: 10px;">
 | 
			
		||||
					<MkButton :class="$style.button" inline @click.stop.prevent="editScheduleNote()"><i class="ti ti-eraser"></i> {{ i18n.ts.deleteAndEdit }}</MkButton>
 | 
			
		||||
					<MkButton :class="$style.button" inline danger @click.stop.prevent="deleteScheduleNote()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -24,18 +28,58 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
 | 
			
		||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
 | 
			
		||||
import MkCwButton from '@/components/MkCwButton.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	note: Misskey.entities.Note & {
 | 
			
		||||
		isSchedule? : boolean,
 | 
			
		||||
		scheduledNoteId?: string
 | 
			
		||||
	};
 | 
			
		||||
	expandAllCws?: boolean;
 | 
			
		||||
	hideFiles?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let showContent = ref(defaultStore.state.uncollapseCW);
 | 
			
		||||
const isDeleted = ref(false);
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'editScheduleNote'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
async function deleteScheduleNote() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.ts.deleteConfirm,
 | 
			
		||||
		okText: i18n.ts.delete,
 | 
			
		||||
		cancelText: i18n.ts.cancel,
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id })
 | 
			
		||||
		.then(() => {
 | 
			
		||||
			isDeleted.value = true;
 | 
			
		||||
		});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function editScheduleNote() {
 | 
			
		||||
	await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id })
 | 
			
		||||
		.then(() => {
 | 
			
		||||
			isDeleted.value = true;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	await os.post({
 | 
			
		||||
		initialNote: props.note,
 | 
			
		||||
		renote: props.note.renote,
 | 
			
		||||
		reply: props.note.reply,
 | 
			
		||||
		channel: props.note.channel,
 | 
			
		||||
	});
 | 
			
		||||
	emit('editScheduleNote');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(() => props.expandAllCws, (expandAllCws) => {
 | 
			
		||||
	if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +94,11 @@ watch(() => props.expandAllCws, (expandAllCws) => {
 | 
			
		|||
	font-size: 0.95em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button{
 | 
			
		||||
	margin-right: var(--margin);
 | 
			
		||||
	margin-bottom: var(--margin);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	display: block;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<div :class="$style.head">
 | 
			
		||||
		<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
 | 
			
		||||
		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
 | 
			
		||||
		<MkAvatar v-if="['pollEnded', 'note', 'edited', 'scheduledNotePosted'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
 | 
			
		||||
		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
 | 
			
		||||
		<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
 | 
			
		||||
		<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
 | 
			
		||||
		<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				[$style.t_exportCompleted]: notification.type === 'exportCompleted',
 | 
			
		||||
				[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
 | 
			
		||||
				[$style.t_pollEnded]: notification.type === 'edited',
 | 
			
		||||
				[$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed',
 | 
			
		||||
				[$style.t_pollEnded]: notification.type === 'scheduledNotePosted',
 | 
			
		||||
			}]"
 | 
			
		||||
		> <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
 | 
			
		||||
			<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<i v-else class="ti ti-badges"></i>
 | 
			
		||||
			</template>
 | 
			
		||||
			<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i>
 | 
			
		||||
			<i v-else-if="notification.type === 'scheduledNoteFailed'" class="ti ti-calendar-event"></i>
 | 
			
		||||
			<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-calendar-event"></i>
 | 
			
		||||
			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
 | 
			
		||||
			<MkReactionIcon
 | 
			
		||||
				v-else-if="notification.type === 'reaction'"
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +74,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'edited'">{{ i18n.ts._notification.edited }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'scheduledNoteFailed'">{{ i18n.ts._notification.scheduledNoteFailed }}</span>
 | 
			
		||||
			<span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span>
 | 
			
		||||
			<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
 | 
			
		||||
		</header>
 | 
			
		||||
		<div>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +115,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
 | 
			
		||||
				{{ i18n.ts.showFile }}
 | 
			
		||||
			</MkA>
 | 
			
		||||
			<div v-else-if="notification.type === 'scheduledNoteFailed'" :class="$style.text">
 | 
			
		||||
				{{ notification.reason }}
 | 
			
		||||
			</div>
 | 
			
		||||
			<template v-else-if="notification.type === 'follow'">
 | 
			
		||||
				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
 | 
			
		||||
			</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +165,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
 | 
			
		||||
				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
 | 
			
		||||
			</MkA>
 | 
			
		||||
 | 
			
		||||
			<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
			
		||||
				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
 | 
			
		||||
				<Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/>
 | 
			
		||||
				<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
 | 
			
		||||
			</MkA>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,6 +77,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"/>
 | 
			
		||||
	<MkScheduleEditor v-if="scheduleNote" v-model="scheduleNote" @destroyed="scheduleNote = 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>
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
 | 
			
		||||
			<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.otherSettings" :class="['_button', $style.footerButton]" @click="showOtherMenu"><i class="ti ti-dots"></i></button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :class="$style.footerRight">
 | 
			
		||||
			<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +112,7 @@ import * as Misskey from 'misskey-js';
 | 
			
		|||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import { toASCII } from 'punycode/';
 | 
			
		||||
import { host, url } from '@@/js/config.js';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
 | 
			
		||||
import MkNotePreview from '@/components/MkNotePreview.vue';
 | 
			
		||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +136,7 @@ import { miLocalStorage } from '@/local-storage.js';
 | 
			
		|||
import { claimAchievement } from '@/scripts/achievements.js';
 | 
			
		||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
 | 
			
		||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
 | 
			
		||||
import MkScheduleEditor from '@/components/MkScheduleEditor.vue';
 | 
			
		||||
 | 
			
		||||
const $i = signinRequired();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +154,9 @@ const props = withDefaults(defineProps<{
 | 
			
		|||
	initialFiles?: Misskey.entities.DriveFile[];
 | 
			
		||||
	initialLocalOnly?: boolean;
 | 
			
		||||
	initialVisibleUsers?: Misskey.entities.UserDetailed[];
 | 
			
		||||
	initialNote?: Misskey.entities.Note;
 | 
			
		||||
	initialNote?: Misskey.entities.Note & {
 | 
			
		||||
		isSchedule?: boolean,
 | 
			
		||||
	};
 | 
			
		||||
	instant?: boolean;
 | 
			
		||||
	fixed?: boolean;
 | 
			
		||||
	autofocus?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -206,6 +212,9 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'
 | 
			
		|||
const imeText = ref('');
 | 
			
		||||
const showingOptions = ref(false);
 | 
			
		||||
const textAreaReadOnly = ref(false);
 | 
			
		||||
const scheduleNote = ref<{
 | 
			
		||||
	scheduledAt: number | null;
 | 
			
		||||
} | null>(null);
 | 
			
		||||
 | 
			
		||||
const draftKey = computed((): string => {
 | 
			
		||||
	let key = props.channel ? `channel:${props.channel.id}` : '';
 | 
			
		||||
| 
						 | 
				
			
			@ -378,6 +387,7 @@ function watchForDraft() {
 | 
			
		|||
	watch(localOnly, () => saveDraft());
 | 
			
		||||
	watch(quoteId, () => saveDraft());
 | 
			
		||||
	watch(reactionAcceptance, () => saveDraft());
 | 
			
		||||
	watch(scheduleNote, () => saveDraft());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MFMWindow() {
 | 
			
		||||
| 
						 | 
				
			
			@ -586,6 +596,7 @@ function clear() {
 | 
			
		|||
	files.value = [];
 | 
			
		||||
	poll.value = null;
 | 
			
		||||
	quoteId.value = null;
 | 
			
		||||
	scheduleNote.value = null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onKeydown(ev: KeyboardEvent) {
 | 
			
		||||
| 
						 | 
				
			
			@ -736,6 +747,7 @@ function saveDraft() {
 | 
			
		|||
			visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
 | 
			
		||||
			quoteId: quoteId.value,
 | 
			
		||||
			reactionAcceptance: reactionAcceptance.value,
 | 
			
		||||
			scheduleNote: scheduleNote.value,
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -843,6 +855,7 @@ async function post(ev?: MouseEvent) {
 | 
			
		|||
		visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
 | 
			
		||||
		reactionAcceptance: reactionAcceptance.value,
 | 
			
		||||
		editId: props.editId ? props.editId : undefined,
 | 
			
		||||
		scheduleNote: scheduleNote.value ?? undefined,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -879,7 +892,7 @@ async function post(ev?: MouseEvent) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	posting.value = true;
 | 
			
		||||
	misskeyApi(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => {
 | 
			
		||||
	misskeyApi(postData.editId ? 'notes/edit' : (postData.scheduleNote ? 'notes/schedule/create' : 'notes/create'), postData, token).then(() => {
 | 
			
		||||
		if (props.freezeAfterPosted) {
 | 
			
		||||
			posted.value = true;
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -1030,6 +1043,42 @@ function openAccountMenu(ev: MouseEvent) {
 | 
			
		|||
	}, ev);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleScheduleNote() {
 | 
			
		||||
	if (scheduleNote.value) {
 | 
			
		||||
		scheduleNote.value = null;
 | 
			
		||||
	} else {
 | 
			
		||||
		scheduleNote.value = {
 | 
			
		||||
			scheduledAt: null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showOtherMenu(ev: MouseEvent) {
 | 
			
		||||
	const menuItems: MenuItem[] = [];
 | 
			
		||||
 | 
			
		||||
	if ($i.policies.scheduleNoteMax > 0) {
 | 
			
		||||
		menuItems.push({
 | 
			
		||||
			type: 'button',
 | 
			
		||||
			text: i18n.ts.schedulePost,
 | 
			
		||||
			icon: 'ti ti-calendar-time',
 | 
			
		||||
			action: toggleScheduleNote,
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'button',
 | 
			
		||||
			text: i18n.ts.schedulePostList,
 | 
			
		||||
			icon: 'ti ti-calendar-event',
 | 
			
		||||
			action: () => {
 | 
			
		||||
				const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSchedulePostListDialog.vue')), {}, {
 | 
			
		||||
					closed: () => {
 | 
			
		||||
						dispose();
 | 
			
		||||
					},
 | 
			
		||||
				});
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	if (props.autofocus) {
 | 
			
		||||
		focus();
 | 
			
		||||
| 
						 | 
				
			
			@ -1099,6 +1148,11 @@ onMounted(() => {
 | 
			
		|||
			}
 | 
			
		||||
			quoteId.value = init.renote ? init.renote.id : null;
 | 
			
		||||
			reactionAcceptance.value = init.reactionAcceptance;
 | 
			
		||||
			if (init.isSchedule) {
 | 
			
		||||
				scheduleNote.value = {
 | 
			
		||||
					scheduledAt: new Date(init.createdAt).getTime(),
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		nextTick(() => watchForDraft());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										65
									
								
								packages/frontend/src/components/MkScheduleEditor.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								packages/frontend/src/components/MkScheduleEditor.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div style="padding: 8px 16px;">
 | 
			
		||||
	<section>
 | 
			
		||||
		<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>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, ref, watch } from 'vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
 | 
			
		||||
import { addTime } from '@/scripts/time.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  modelValue: {
 | 
			
		||||
		scheduledAt: number | null;
 | 
			
		||||
  };
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (ev: 'update:modelValue', v: {
 | 
			
		||||
		scheduledAt: number | null;
 | 
			
		||||
  }): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
 | 
			
		||||
const atTime = ref('00:00');
 | 
			
		||||
 | 
			
		||||
if (props.modelValue.scheduledAt) {
 | 
			
		||||
	const date = new Date(props.modelValue.scheduledAt);
 | 
			
		||||
	atDate.value = formatDateTimeString(date, 'yyyy-MM-dd');
 | 
			
		||||
	atTime.value = formatDateTimeString(date, 'HH:mm');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get() {
 | 
			
		||||
	const calcAt = () => {
 | 
			
		||||
		return new Date(`${ atDate.value } ${ atTime.value }`).getTime();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return { scheduledAt: calcAt() };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch([
 | 
			
		||||
	atDate,
 | 
			
		||||
	atTime,
 | 
			
		||||
], () => emit('update:modelValue', get()), {
 | 
			
		||||
	deep: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	emit('update:modelValue', get());
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialogEl"
 | 
			
		||||
	:withOkButton="false"
 | 
			
		||||
	@click="cancel()"
 | 
			
		||||
	@close="cancel()"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ i18n.ts.schedulePostList }}</template>
 | 
			
		||||
	<MkSpacer :marginMin="14" :marginMax="16">
 | 
			
		||||
		<MkPagination ref="paginationEl" :pagination="pagination">
 | 
			
		||||
			<template #empty>
 | 
			
		||||
				<div class="_fullinfo">
 | 
			
		||||
					<img :src="infoImageUrl" class="_ghost"/>
 | 
			
		||||
					<div>{{ i18n.ts.nothing }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
 | 
			
		||||
			<template #default="{ items }">
 | 
			
		||||
				<div class="_gaps">
 | 
			
		||||
					<MkNoteSimple v-for="item in items" :key="item.id" :scheduled="true" :note="item.note" @editScheduleNote="listUpdate"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import type { Paging } from '@/components/MkPagination.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { infoImageUrl } from '@/instance.js';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'cancel'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialogEl = ref();
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
	emit('cancel');
 | 
			
		||||
	dialogEl.value.close();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const paginationEl = ref();
 | 
			
		||||
const pagination: Paging = {
 | 
			
		||||
	endpoint: 'notes/schedule/list',
 | 
			
		||||
	limit: 10,
 | 
			
		||||
	offsetMode: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function listUpdate() {
 | 
			
		||||
	paginationEl.value.reload();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -733,3 +733,4 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> {
 | 
			
		|||
		});
 | 
			
		||||
	});
 | 
			
		||||
}*/
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -200,6 +200,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])">
 | 
			
		||||
				<template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
					<span v-if="role.policies.scheduleNoteMax.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
 | 
			
		||||
					<span v-else>{{ role.policies.scheduleNoteMax.value }}</span>
 | 
			
		||||
					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduleNoteMax)"></i></span>
 | 
			
		||||
				</template>
 | 
			
		||||
				<div class="_gaps">
 | 
			
		||||
					<MkSwitch v-model="role.policies.scheduleNoteMax.useDefault" :readonly="readonly">
 | 
			
		||||
						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 | 
			
		||||
					</MkSwitch>
 | 
			
		||||
					<MkInput v-model="role.policies.scheduleNoteMax.value" :disabled="role.policies.scheduleNoteMax.useDefault" type="number" :readonly="readonly">
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<MkRange v-model="role.policies.scheduleNoteMax.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
 | 
			
		||||
						<template #label>{{ i18n.ts._role.priority }}</template>
 | 
			
		||||
					</MkRange>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
 | 
			
		||||
				<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,6 +70,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
							</MkSwitch>
 | 
			
		||||
						</MkFolder>
 | 
			
		||||
 | 
			
		||||
						<MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])">
 | 
			
		||||
							<template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template>
 | 
			
		||||
							<template #suffix>{{ policies.scheduleNoteMax }}</template>
 | 
			
		||||
							<MkInput v-model="policies.scheduleNoteMax" type="number">
 | 
			
		||||
							</MkInput>
 | 
			
		||||
						</MkFolder>
 | 
			
		||||
 | 
			
		||||
						<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
 | 
			
		||||
							<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
 | 
			
		||||
							<template #suffix>{{ policies.mentionLimit }}</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,6 +175,7 @@ export function pluginReplaceIcons() {
 | 
			
		|||
					'ti ti-cake': 'ph-cake ph-bold ph-lg',
 | 
			
		||||
					'ti ti-calendar': 'ph-calendar ph-bold ph-lg',
 | 
			
		||||
					'ti ti-calendar-time': 'ph-calendar ph-bold ph-lg',
 | 
			
		||||
					'ti ti-calendar-event': 'ph-calendar-star ph-bold ph-lg',
 | 
			
		||||
					'ti ti-camera': 'ph-camera ph-bold ph-lg',
 | 
			
		||||
					'ti ti-carousel-horizontal': 'ph-split-horizontal ph-bold ph-lg',
 | 
			
		||||
					'ti ti-carousel-vertical': 'ph-split-vertical ph-bold ph-lg',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1684,6 +1684,10 @@ declare namespace entities {
 | 
			
		|||
        NotesRenotesResponse,
 | 
			
		||||
        NotesRepliesRequest,
 | 
			
		||||
        NotesRepliesResponse,
 | 
			
		||||
        NotesScheduleCreateRequest,
 | 
			
		||||
        NotesScheduleDeleteRequest,
 | 
			
		||||
        NotesScheduleListRequest,
 | 
			
		||||
        NotesScheduleListResponse,
 | 
			
		||||
        NotesSearchByTagRequest,
 | 
			
		||||
        NotesSearchByTagResponse,
 | 
			
		||||
        NotesSearchRequest,
 | 
			
		||||
| 
						 | 
				
			
			@ -2807,6 +2811,18 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j
 | 
			
		|||
// @public (undocumented)
 | 
			
		||||
type NotesResponse = operations['notes']['responses']['200']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2874,7 +2890,7 @@ type Notification_2 = components['schemas']['Notification'];
 | 
			
		|||
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited"];
 | 
			
		||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited", "scheduledNoteFailed", "scheduledNotePosted"];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
export function nyaize(text: string): string;
 | 
			
		||||
| 
						 | 
				
			
			@ -2937,7 +2953,7 @@ type PartialRolePolicyOverride = Partial<{
 | 
			
		|||
}>;
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
 | 
			
		||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
 | 
			
		||||
 | 
			
		||||
// @public (undocumented)
 | 
			
		||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3385,6 +3385,39 @@ declare module '../api.js' {
 | 
			
		|||
      credential?: string | null,
 | 
			
		||||
    ): Promise<SwitchCaseResponseType<E, P>>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * No description provided.
 | 
			
		||||
     * 
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    request<E extends 'notes/schedule/create', P extends Endpoints[E]['req']>(
 | 
			
		||||
      endpoint: E,
 | 
			
		||||
      params: P,
 | 
			
		||||
      credential?: string | null,
 | 
			
		||||
    ): Promise<SwitchCaseResponseType<E, P>>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * No description provided.
 | 
			
		||||
     * 
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    request<E extends 'notes/schedule/delete', P extends Endpoints[E]['req']>(
 | 
			
		||||
      endpoint: E,
 | 
			
		||||
      params: P,
 | 
			
		||||
      credential?: string | null,
 | 
			
		||||
    ): Promise<SwitchCaseResponseType<E, P>>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * No description provided.
 | 
			
		||||
     * 
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *read:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    request<E extends 'notes/schedule/list', P extends Endpoints[E]['req']>(
 | 
			
		||||
      endpoint: E,
 | 
			
		||||
      params: P,
 | 
			
		||||
      credential?: string | null,
 | 
			
		||||
    ): Promise<SwitchCaseResponseType<E, P>>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * No description provided.
 | 
			
		||||
     * 
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -450,6 +450,10 @@ import type {
 | 
			
		|||
	NotesRenotesResponse,
 | 
			
		||||
	NotesRepliesRequest,
 | 
			
		||||
	NotesRepliesResponse,
 | 
			
		||||
	NotesScheduleCreateRequest,
 | 
			
		||||
	NotesScheduleDeleteRequest,
 | 
			
		||||
	NotesScheduleListRequest,
 | 
			
		||||
	NotesScheduleListResponse,
 | 
			
		||||
	NotesSearchByTagRequest,
 | 
			
		||||
	NotesSearchByTagResponse,
 | 
			
		||||
	NotesSearchRequest,
 | 
			
		||||
| 
						 | 
				
			
			@ -900,6 +904,9 @@ export type Endpoints = {
 | 
			
		|||
	'notes/like': { req: NotesLikeRequest; res: EmptyResponse };
 | 
			
		||||
	'notes/renotes': { req: NotesRenotesRequest; res: NotesRenotesResponse };
 | 
			
		||||
	'notes/replies': { req: NotesRepliesRequest; res: NotesRepliesResponse };
 | 
			
		||||
	'notes/schedule/create': { req: NotesScheduleCreateRequest; res: EmptyResponse };
 | 
			
		||||
	'notes/schedule/delete': { req: NotesScheduleDeleteRequest; res: EmptyResponse };
 | 
			
		||||
	'notes/schedule/list': { req: NotesScheduleListRequest; res: NotesScheduleListResponse };
 | 
			
		||||
	'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse };
 | 
			
		||||
	'notes/search': { req: NotesSearchRequest; res: NotesSearchResponse };
 | 
			
		||||
	'notes/show': { req: NotesShowRequest; res: NotesShowResponse };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -453,6 +453,10 @@ export type NotesRenotesRequest = operations['notes___renotes']['requestBody']['
 | 
			
		|||
export type NotesRenotesResponse = operations['notes___renotes']['responses']['200']['content']['application/json'];
 | 
			
		||||
export type NotesRepliesRequest = operations['notes___replies']['requestBody']['content']['application/json'];
 | 
			
		||||
export type NotesRepliesResponse = operations['notes___replies']['responses']['200']['content']['application/json'];
 | 
			
		||||
export type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json'];
 | 
			
		||||
export type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json'];
 | 
			
		||||
export type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json'];
 | 
			
		||||
export type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json'];
 | 
			
		||||
export type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json'];
 | 
			
		||||
export type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json'];
 | 
			
		||||
export type NotesSearchRequest = operations['notes___search']['requestBody']['content']['application/json'];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2935,6 +2935,33 @@ export type paths = {
 | 
			
		|||
     */
 | 
			
		||||
    post: operations['notes___replies'];
 | 
			
		||||
  };
 | 
			
		||||
  '/notes/schedule/create': {
 | 
			
		||||
    /**
 | 
			
		||||
     * notes/schedule/create
 | 
			
		||||
     * @description No description provided.
 | 
			
		||||
     *
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    post: operations['notes___schedule___create'];
 | 
			
		||||
  };
 | 
			
		||||
  '/notes/schedule/delete': {
 | 
			
		||||
    /**
 | 
			
		||||
     * notes/schedule/delete
 | 
			
		||||
     * @description No description provided.
 | 
			
		||||
     *
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    post: operations['notes___schedule___delete'];
 | 
			
		||||
  };
 | 
			
		||||
  '/notes/schedule/list': {
 | 
			
		||||
    /**
 | 
			
		||||
     * notes/schedule/list
 | 
			
		||||
     * @description No description provided.
 | 
			
		||||
     *
 | 
			
		||||
     * **Credential required**: *Yes* / **Permission**: *read:notes-schedule*
 | 
			
		||||
     */
 | 
			
		||||
    post: operations['notes___schedule___list'];
 | 
			
		||||
  };
 | 
			
		||||
  '/notes/search-by-tag': {
 | 
			
		||||
    /**
 | 
			
		||||
     * notes/search-by-tag
 | 
			
		||||
| 
						 | 
				
			
			@ -4490,6 +4517,25 @@ export type components = {
 | 
			
		|||
      /** Format: id */
 | 
			
		||||
      userId: string;
 | 
			
		||||
      note: components['schemas']['Note'];
 | 
			
		||||
    } | {
 | 
			
		||||
      /** Format: id */
 | 
			
		||||
      id: string;
 | 
			
		||||
      /** Format: date-time */
 | 
			
		||||
      createdAt: string;
 | 
			
		||||
      /** @enum {string} */
 | 
			
		||||
      type: 'scheduledNoteFailed';
 | 
			
		||||
      reason: string;
 | 
			
		||||
    } | {
 | 
			
		||||
      /** Format: id */
 | 
			
		||||
      id: string;
 | 
			
		||||
      /** Format: date-time */
 | 
			
		||||
      createdAt: string;
 | 
			
		||||
      /** @enum {string} */
 | 
			
		||||
      type: 'scheduledNotePosted';
 | 
			
		||||
      user: components['schemas']['UserLite'];
 | 
			
		||||
      /** Format: id */
 | 
			
		||||
      userId: string;
 | 
			
		||||
      note: components['schemas']['Note'];
 | 
			
		||||
    } | {
 | 
			
		||||
      /** Format: id */
 | 
			
		||||
      id: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -5036,6 +5082,7 @@ export type components = {
 | 
			
		|||
      canImportFollowing: boolean;
 | 
			
		||||
      canImportMuting: boolean;
 | 
			
		||||
      canImportUserLists: boolean;
 | 
			
		||||
      scheduleNoteMax: number;
 | 
			
		||||
    };
 | 
			
		||||
    ReversiGameLite: {
 | 
			
		||||
      /** Format: id */
 | 
			
		||||
| 
						 | 
				
			
			@ -19956,8 +20003,8 @@ export type operations = {
 | 
			
		|||
          untilId?: string;
 | 
			
		||||
          /** @default true */
 | 
			
		||||
          markAsRead?: boolean;
 | 
			
		||||
          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -20024,8 +20071,8 @@ export type operations = {
 | 
			
		|||
          untilId?: string;
 | 
			
		||||
          /** @default true */
 | 
			
		||||
          markAsRead?: boolean;
 | 
			
		||||
          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
          excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -24424,6 +24471,239 @@ export type operations = {
 | 
			
		|||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  /**
 | 
			
		||||
   * notes/schedule/create
 | 
			
		||||
   * @description No description provided.
 | 
			
		||||
   *
 | 
			
		||||
   * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
   */
 | 
			
		||||
  notes___schedule___create: {
 | 
			
		||||
    requestBody: {
 | 
			
		||||
      content: {
 | 
			
		||||
        'application/json': {
 | 
			
		||||
          /**
 | 
			
		||||
           * @default public
 | 
			
		||||
           * @enum {string}
 | 
			
		||||
           */
 | 
			
		||||
          visibility?: 'public' | 'home' | 'followers' | 'specified';
 | 
			
		||||
          visibleUserIds?: string[];
 | 
			
		||||
          cw?: string | null;
 | 
			
		||||
          /**
 | 
			
		||||
           * @default null
 | 
			
		||||
           * @enum {string|null}
 | 
			
		||||
           */
 | 
			
		||||
          reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
 | 
			
		||||
          /** @default false */
 | 
			
		||||
          noExtractMentions?: boolean;
 | 
			
		||||
          /** @default false */
 | 
			
		||||
          noExtractHashtags?: boolean;
 | 
			
		||||
          /** @default false */
 | 
			
		||||
          noExtractEmojis?: boolean;
 | 
			
		||||
          /** Format: misskey:id */
 | 
			
		||||
          replyId?: string | null;
 | 
			
		||||
          /** Format: misskey:id */
 | 
			
		||||
          renoteId?: string | null;
 | 
			
		||||
          text?: string | null;
 | 
			
		||||
          fileIds?: string[];
 | 
			
		||||
          mediaIds?: string[];
 | 
			
		||||
          poll?: ({
 | 
			
		||||
            choices: string[];
 | 
			
		||||
            multiple?: boolean;
 | 
			
		||||
            expiresAt?: number | null;
 | 
			
		||||
            expiredAfter?: number | null;
 | 
			
		||||
          }) | null;
 | 
			
		||||
          scheduleNote: {
 | 
			
		||||
            scheduledAt?: number;
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
    responses: {
 | 
			
		||||
      /** @description OK (without any results) */
 | 
			
		||||
      204: {
 | 
			
		||||
        content: never;
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Client error */
 | 
			
		||||
      400: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Authentication error */
 | 
			
		||||
      401: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Forbidden error */
 | 
			
		||||
      403: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description I'm Ai */
 | 
			
		||||
      418: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description To many requests */
 | 
			
		||||
      429: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Internal server error */
 | 
			
		||||
      500: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  /**
 | 
			
		||||
   * notes/schedule/delete
 | 
			
		||||
   * @description No description provided.
 | 
			
		||||
   *
 | 
			
		||||
   * **Credential required**: *Yes* / **Permission**: *write:notes-schedule*
 | 
			
		||||
   */
 | 
			
		||||
  notes___schedule___delete: {
 | 
			
		||||
    requestBody: {
 | 
			
		||||
      content: {
 | 
			
		||||
        'application/json': {
 | 
			
		||||
          /** Format: misskey:id */
 | 
			
		||||
          noteId: string;
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
    responses: {
 | 
			
		||||
      /** @description OK (without any results) */
 | 
			
		||||
      204: {
 | 
			
		||||
        content: never;
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Client error */
 | 
			
		||||
      400: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Authentication error */
 | 
			
		||||
      401: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Forbidden error */
 | 
			
		||||
      403: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description I'm Ai */
 | 
			
		||||
      418: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description To many requests */
 | 
			
		||||
      429: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Internal server error */
 | 
			
		||||
      500: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  /**
 | 
			
		||||
   * notes/schedule/list
 | 
			
		||||
   * @description No description provided.
 | 
			
		||||
   *
 | 
			
		||||
   * **Credential required**: *Yes* / **Permission**: *read:notes-schedule*
 | 
			
		||||
   */
 | 
			
		||||
  notes___schedule___list: {
 | 
			
		||||
    requestBody: {
 | 
			
		||||
      content: {
 | 
			
		||||
        'application/json': {
 | 
			
		||||
          /** Format: misskey:id */
 | 
			
		||||
          sinceId?: string;
 | 
			
		||||
          /** Format: misskey:id */
 | 
			
		||||
          untilId?: string;
 | 
			
		||||
          /** @default 10 */
 | 
			
		||||
          limit?: number;
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
    responses: {
 | 
			
		||||
      /** @description OK (with results) */
 | 
			
		||||
      200: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': ({
 | 
			
		||||
              /** Format: misskey:id */
 | 
			
		||||
              id: string;
 | 
			
		||||
              note: {
 | 
			
		||||
                createdAt: string;
 | 
			
		||||
                text?: string;
 | 
			
		||||
                cw?: string | null;
 | 
			
		||||
                fileIds: string[];
 | 
			
		||||
                /** @enum {string} */
 | 
			
		||||
                visibility: 'public' | 'home' | 'followers' | 'specified';
 | 
			
		||||
                visibleUsers: components['schemas']['UserLite'][];
 | 
			
		||||
                user: components['schemas']['User'];
 | 
			
		||||
                /**
 | 
			
		||||
                 * @default null
 | 
			
		||||
                 * @enum {string|null}
 | 
			
		||||
                 */
 | 
			
		||||
                reactionAcceptance: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
 | 
			
		||||
                isSchedule: boolean;
 | 
			
		||||
              };
 | 
			
		||||
              userId: string;
 | 
			
		||||
              scheduledAt: string;
 | 
			
		||||
            })[];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Client error */
 | 
			
		||||
      400: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Authentication error */
 | 
			
		||||
      401: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Forbidden error */
 | 
			
		||||
      403: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description I'm Ai */
 | 
			
		||||
      418: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description To many requests */
 | 
			
		||||
      429: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
      /** @description Internal server error */
 | 
			
		||||
      500: {
 | 
			
		||||
        content: {
 | 
			
		||||
          'application/json': components['schemas']['Error'];
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  /**
 | 
			
		||||
   * notes/search-by-tag
 | 
			
		||||
   * @description No description provided.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ import type {
 | 
			
		|||
	UserLite,
 | 
			
		||||
} from './autogen/models.js';
 | 
			
		||||
 | 
			
		||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const;
 | 
			
		||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed', 'scheduledNotePosted'] as const;
 | 
			
		||||
 | 
			
		||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +42,8 @@ export const permissions = [
 | 
			
		|||
	'read:mutes',
 | 
			
		||||
	'write:mutes',
 | 
			
		||||
	'write:notes',
 | 
			
		||||
	'read:notes-schedule',
 | 
			
		||||
	'write:notes-schedule',
 | 
			
		||||
	'read:notifications',
 | 
			
		||||
	'write:notifications',
 | 
			
		||||
	'read:reactions',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -258,6 +258,21 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 | 
			
		|||
						data,
 | 
			
		||||
					}];
 | 
			
		||||
 | 
			
		||||
				case 'scheduledNoteFailed':
 | 
			
		||||
					return [i18n.ts._notification.scheduledNoteFailed, {
 | 
			
		||||
						body: data.body.reason,
 | 
			
		||||
						badge: iconUrl('bell'),
 | 
			
		||||
						data,
 | 
			
		||||
					}];
 | 
			
		||||
 | 
			
		||||
				case 'scheduledNotePosted':
 | 
			
		||||
					return [i18n.ts._notification.scheduledNotePosted, {
 | 
			
		||||
						body: data.body.note.text ?? '',
 | 
			
		||||
						icon: data.body.user.avatarUrl ?? undefined,
 | 
			
		||||
						badge: iconUrl('bell'),
 | 
			
		||||
						data,
 | 
			
		||||
					}];
 | 
			
		||||
 | 
			
		||||
				default:
 | 
			
		||||
					return null;
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,6 +225,7 @@ _role:
 | 
			
		|||
    btlAvailable: "Can view the bubble timeline"
 | 
			
		||||
    canImportNotes: "Can import notes"
 | 
			
		||||
    canUpdateBioMedia: "Allow users to edit their avatar or banner"
 | 
			
		||||
    scheduleNoteMax: "Maximum number of scheduled notes"
 | 
			
		||||
  _condition:
 | 
			
		||||
    isLocked: "Private account"
 | 
			
		||||
    isExplorable: "Account is discoverable"
 | 
			
		||||
| 
						 | 
				
			
			@ -276,6 +277,8 @@ _notification:
 | 
			
		|||
  youRenoted: "Boost from {name}"
 | 
			
		||||
  renotedBySomeUsers: "Boosted by {n} users"
 | 
			
		||||
  edited: "Note got edited"
 | 
			
		||||
  scheduledNoteFailed: "Posting scheduled note failed"
 | 
			
		||||
  scheduledNotePosted: "Scheduled Note was posted"
 | 
			
		||||
  _types:
 | 
			
		||||
    renote: "Boosts"
 | 
			
		||||
    edited: "Edits"
 | 
			
		||||
| 
						 | 
				
			
			@ -414,3 +417,10 @@ _deck:
 | 
			
		|||
    following: "Following"
 | 
			
		||||
 | 
			
		||||
selectFollowRelationship: "Select a follow relationship..."
 | 
			
		||||
 | 
			
		||||
schedulePost: "Schedule a note"
 | 
			
		||||
schedulePostList: "List of scheduled notes"
 | 
			
		||||
 | 
			
		||||
_permissions:
 | 
			
		||||
  "read:notes-schedule": "View your list of scheduled notes"
 | 
			
		||||
  "write:notes-schedule": "Compose or delete scheduled notes"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -210,6 +210,7 @@ _role:
 | 
			
		|||
    btlAvailable: "バブルタイムラインの閲覧"
 | 
			
		||||
    canImportNotes: "ノートのインポートが可能"
 | 
			
		||||
    canUpdateBioMedia: "アイコンとバナーの更新を許可"
 | 
			
		||||
    scheduleNoteMax: "予約投稿の最大数"
 | 
			
		||||
  _condition:
 | 
			
		||||
    isLocked: "鍵アカウントユーザー"
 | 
			
		||||
    isExplorable: "「アカウントを見つけやすくする」が有効なユーザー"
 | 
			
		||||
| 
						 | 
				
			
			@ -384,3 +385,8 @@ _externalNavigationWarning:
 | 
			
		|||
  title: "外部サイトに移動します"
 | 
			
		||||
  description: "{host}を離れて外部サイトに移動します"
 | 
			
		||||
  trustThisDomain: "このデバイスで今後このドメインを信頼する"
 | 
			
		||||
schedulePost: "予約投稿"
 | 
			
		||||
schedulePostList: "予約投稿一覧"
 | 
			
		||||
_permissions:
 | 
			
		||||
  "read:notes-schedule": "予約投稿を見る"
 | 
			
		||||
  "write:notes-schedule": "予約投稿を作成・削除する"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue