summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/NoteCreateService.ts
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-09-26 15:29:52 +0900
committerGitHub <noreply@github.com>2025-09-26 15:29:52 +0900
commitd1446d195abb52c560c7c97177d08103a175acf7 (patch)
tree355689adb542333f4c5abb186ab9819c29274612 /packages/backend/src/core/NoteCreateService.ts
parentfix(frontend): ビルド成果物のファイル名にlocalesのhashを含め... (diff)
downloadmisskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.gz
misskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.bz2
misskey-d1446d195abb52c560c7c97177d08103a175acf7.zip
feat: scheduled post (#16577)
* Update NoteDraft.ts * Update NoteDraft.ts * wip * Update CHANGELOG.md * wip * Update PostScheduledNoteProcessorService.ts * Update PostScheduledNoteProcessorService.ts * Update Notification.ts * wip * Update NoteDraftService.ts * Update NoteDraftService.ts * Update NoteDraftService.ts * wip * Create 1758677617888-scheduled-post.js * Update index.d.ts * Update stats.ts * wip * wip * wip * wip * wip * Update MkNotification.vue * wip * wip * wip * Update NoteDraftService.ts * Update NoteDraftService.ts * wip * wip * Update NoteDraftEntityService.ts * wip * Update index.d.ts * Update MkPostForm.vue * wip * wip * wip * Update NoteCreateService.ts * wip * wip * wip * Update NoteDraftEntityService.ts * Update NoteCreateService.ts * Update NoteDraftService.ts * wip * Update NoteDraftService.ts * wip * wip * Update MkPostForm.vue * wip * Update MkPostForm.vue * Update os.ts * wip * Update MkNoteDraftsDialog.vue
Diffstat (limited to 'packages/backend/src/core/NoteCreateService.ts')
-rw-r--r--packages/backend/src/core/NoteCreateService.ts170
1 files changed, 169 insertions, 1 deletions
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 1eefcfa054..b6acf4c5fb 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
-import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
@@ -222,6 +229,167 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
+ public async fetchAndCreate(user: {
+ id: MiUser['id'];
+ username: MiUser['username'];
+ host: MiUser['host'];
+ isBot: MiUser['isBot'];
+ isCat: MiUser['isCat'];
+ }, data: {
+ createdAt: Date;
+ replyId: MiNote['id'] | null;
+ renoteId: MiNote['id'] | null;
+ fileIds: MiDriveFile['id'][];
+ text: string | null;
+ cw: string | null;
+ visibility: string;
+ visibleUserIds: MiUser['id'][];
+ channelId: MiChannel['id'] | null;
+ localOnly: boolean;
+ reactionAcceptance: MiNote['reactionAcceptance'];
+ poll: IPoll | null;
+ apMentions?: MinimumUser[] | null;
+ apHashtags?: string[] | null;
+ apEmojis?: string[] | null;
+ }): Promise<MiNote> {
+ const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({
+ id: In(data.visibleUserIds),
+ }) : [];
+
+ let files: MiDriveFile[] = [];
+ if (data.fileIds.length > 0) {
+ files = await this.driveFilesRepository.createQueryBuilder('file')
+ .where('file.userId = :userId AND file.id IN (:...fileIds)', {
+ userId: user.id,
+ fileIds: data.fileIds,
+ })
+ .orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
+ .setParameters({ fileIds: data.fileIds })
+ .getMany();
+
+ if (files.length !== data.fileIds.length) {
+ throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file');
+ }
+ }
+
+ let renote: MiNote | null = null;
+ if (data.renoteId != null) {
+ // Fetch renote to note
+ renote = await this.notesRepository.findOne({
+ where: { id: data.renoteId },
+ relations: ['user', 'renote', 'reply'],
+ });
+
+ if (renote == null) {
+ throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target');
+ } else if (isRenote(renote) && !isQuote(renote)) {
+ throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote');
+ }
+
+ // Check blocking
+ if (renote.userId !== user.id) {
+ const blockExist = await this.blockingsRepository.exists({
+ where: {
+ blockerId: renote.userId,
+ blockeeId: user.id,
+ },
+ });
+ if (blockExist) {
+ throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user');
+ }
+ }
+
+ if (renote.visibility === 'followers' && renote.userId !== user.id) {
+ // 他人のfollowers noteはreject
+ throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility');
+ } else if (renote.visibility === 'specified') {
+ // specified / direct noteはreject
+ throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility');
+ }
+
+ if (renote.channelId && renote.channelId !== data.channelId) {
+ // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
+ // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
+ const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
+ if (renoteChannel == null) {
+ // リノートしたいノートが書き込まれているチャンネルが無い
+ throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel');
+ } else if (!renoteChannel.allowRenoteToExternal) {
+ // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
+ throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external');
+ }
+ }
+ }
+
+ let reply: MiNote | null = null;
+ if (data.replyId != null) {
+ // Fetch reply
+ reply = await this.notesRepository.findOne({
+ where: { id: data.replyId },
+ relations: ['user'],
+ });
+
+ if (reply == null) {
+ throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target');
+ } else if (isRenote(reply) && !isQuote(reply)) {
+ throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote');
+ } else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) {
+ throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target');
+ } else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
+ throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility');
+ }
+
+ // Check blocking
+ if (reply.userId !== user.id) {
+ const blockExist = await this.blockingsRepository.exists({
+ where: {
+ blockerId: reply.userId,
+ blockeeId: user.id,
+ },
+ });
+ if (blockExist) {
+ throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user');
+ }
+ }
+ }
+
+ if (data.poll) {
+ if (data.poll.expiresAt != null) {
+ if (data.poll.expiresAt.getTime() < Date.now()) {
+ throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time');
+ }
+ }
+ }
+
+ let channel: MiChannel | null = null;
+ if (data.channelId != null) {
+ channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false });
+
+ if (channel == null) {
+ throw new IdentifiableError('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel');
+ }
+ }
+
+ return this.create(user, {
+ createdAt: data.createdAt,
+ files: files,
+ poll: data.poll,
+ text: data.text,
+ reply,
+ renote,
+ cw: data.cw,
+ localOnly: data.localOnly,
+ reactionAcceptance: data.reactionAcceptance,
+ visibility: data.visibility,
+ visibleUsers,
+ channel,
+ apMentions: data.apMentions,
+ apHashtags: data.apHashtags,
+ apEmojis: data.apEmojis,
+ });
+ }
+
+ @bindThis
public async create(user: {
id: MiUser['id'];
username: MiUser['username'];