summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints/notes
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-09-18 03:27:08 +0900
committerGitHub <noreply@github.com>2022-09-18 03:27:08 +0900
commitb75184ec8e3436200bacdcd832e3324702553d20 (patch)
tree8b7e316f29e95df921db57289c8b8da476d18f07 /packages/backend/src/server/api/endpoints/notes
parentUpdate ROADMAP.md (diff)
downloadsharkey-b75184ec8e3436200bacdcd832e3324702553d20.tar.gz
sharkey-b75184ec8e3436200bacdcd832e3324702553d20.tar.bz2
sharkey-b75184ec8e3436200bacdcd832e3324702553d20.zip
なんかもうめっちゃ変えた
Diffstat (limited to 'packages/backend/src/server/api/endpoints/notes')
-rw-r--r--packages/backend/src/server/api/endpoints/notes/children.ts85
-rw-r--r--packages/backend/src/server/api/endpoints/notes/clips.ts51
-rw-r--r--packages/backend/src/server/api/endpoints/notes/conversation.ts74
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts234
-rw-r--r--packages/backend/src/server/api/endpoints/notes/delete.ts43
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/create.ts65
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/delete.ts52
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts81
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts112
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts175
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts137
-rw-r--r--packages/backend/src/server/api/endpoints/notes/mentions.ts101
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts112
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/vote.ts194
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions.ts66
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions/create.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions/delete.ts35
-rw-r--r--packages/backend/src/server/api/endpoints/notes/renotes.ts74
-rw-r--r--packages/backend/src/server/api/endpoints/notes/replies.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts145
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search.ts139
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/notes/state.ts74
-rw-r--r--packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts177
-rw-r--r--packages/backend/src/server/api/endpoints/notes/translate.ts111
-rw-r--r--packages/backend/src/server/api/endpoints/notes/unrenote.ts48
-rw-r--r--packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts148
-rw-r--r--packages/backend/src/server/api/endpoints/notes/watching/create.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/notes/watching/delete.ts38
31 files changed, 1561 insertions, 1293 deletions
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index efc109105c..86f90e049f 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -1,10 +1,10 @@
import { Brackets } from 'typeorm';
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -34,38 +34,49 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere(new Brackets(qb => { qb
- .where('note.replyId = :noteId', { noteId: ps.noteId })
- .orWhere(new Brackets(qb => { qb
- .where('note.renoteId = :noteId', { noteId: ps.noteId })
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
- .where('note.text IS NOT NULL')
- .orWhere('note.fileIds != \'{}\'')
- .orWhere('note.hasPoll = TRUE');
- }));
- }));
- }))
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ .where('note.replyId = :noteId', { noteId: ps.noteId })
+ .orWhere(new Brackets(qb => { qb
+ .where('note.renoteId = :noteId', { noteId: ps.noteId })
+ .andWhere(new Brackets(qb => { qb
+ .where('note.text IS NOT NULL')
+ .orWhere('note.fileIds != \'{}\'')
+ .orWhere('note.hasPoll = TRUE');
+ }));
+ }));
+ }))
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- generateVisibilityQuery(query, user);
- if (user) {
- generateMutedUserQuery(query, user);
- generateBlockedUserQuery(query, user);
- }
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) {
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
- const notes = await query.take(ps.limit).getMany();
+ const notes = await query.take(ps.limit).getMany();
- return await Notes.packMany(notes, user);
-});
+ return await this.noteEntityService.packMany(notes, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts
index e79f8563e8..7d893f32a1 100644
--- a/packages/backend/src/server/api/endpoints/notes/clips.ts
+++ b/packages/backend/src/server/api/endpoints/notes/clips.ts
@@ -1,8 +1,11 @@
import { In } from 'typeorm';
-import { ClipNotes, Clips } from '@/models/index.js';
-import define from '../../define.js';
-import { getNote } from '../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['clips', 'notes'],
@@ -37,20 +40,34 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, me) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.clipsRepository)
+ private clipsRepository: ClipsRepository,
- const clipNotes = await ClipNotes.findBy({
- noteId: note.id,
- });
+ @Inject(DI.clipNotesRepository)
+ private clipNotesRepository: ClipNotesRepository,
- const clips = await Clips.findBy({
- id: In(clipNotes.map(x => x.clipId)),
- isPublic: true,
- });
+ private clipEntityService: ClipEntityService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- return await Promise.all(clips.map(x => Clips.pack(x)));
-});
+ const clipNotes = await this.clipNotesRepository.findBy({
+ noteId: note.id,
+ });
+
+ const clips = await this.clipsRepository.findBy({
+ id: In(clipNotes.map(x => x.clipId)),
+ isPublic: true,
+ });
+
+ return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts
index b731d18248..2f8324ed62 100644
--- a/packages/backend/src/server/api/endpoints/notes/conversation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts
@@ -1,8 +1,11 @@
-import { Note } from '@/models/entities/note.js';
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import type { Note } from '@/models/entities/Note.js';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { getNote } from '../../common/getters.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -39,36 +42,47 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const conversation: Note[] = [];
- let i = 0;
+ private noteEntityService: NoteEntityService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- async function get(id: any) {
- i++;
- const p = await Notes.findOneBy({ id });
- if (p == null) return;
+ const conversation: Note[] = [];
+ let i = 0;
- if (i > ps.offset!) {
- conversation.push(p);
- }
+ const get = async (id: any) => {
+ i++;
+ const p = await this.notesRepository.findOneBy({ id });
+ if (p == null) return;
- if (conversation.length === ps.limit) {
- return;
- }
+ if (i > ps.offset!) {
+ conversation.push(p);
+ }
- if (p.replyId) {
- await get(p.replyId);
- }
- }
+ if (conversation.length === ps.limit) {
+ return;
+ }
- if (note.replyId) {
- await get(note.replyId);
- }
+ if (p.replyId) {
+ await get(p.replyId);
+ }
+ };
- return await Notes.packMany(conversation, user);
-});
+ if (note.replyId) {
+ await get(note.replyId);
+ }
+
+ return await this.noteEntityService.packMany(conversation, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index a133294169..30b7a889fc 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -1,15 +1,18 @@
import ms from 'ms';
import { In } from 'typeorm';
-import create from '@/services/note/create.js';
-import { User } from '@/models/entities/user.js';
-import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js';
-import { DriveFile } from '@/models/entities/drive-file.js';
-import { Note } from '@/models/entities/note.js';
-import { Channel } from '@/models/entities/channel.js';
+import { Inject, Injectable } from '@nestjs/common';
+import type { User } from '@/models/entities/User.js';
+import { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
+import type { DriveFile } from '@/models/entities/DriveFile.js';
+import type { Note } from '@/models/entities/Note.js';
+import type { Channel } from '@/models/entities/Channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { DI } from '@/di-symbols.js';
import { noteVisibilities } from '../../../../types.js';
import { ApiError } from '../../error.js';
-import define from '../../define.js';
export const meta = {
tags: ['notes'],
@@ -161,115 +164,138 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- let visibleUsers: User[] = [];
- if (ps.visibleUserIds) {
- visibleUsers = await Users.findBy({
- id: In(ps.visibleUserIds),
- });
- }
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
- let files: DriveFile[] = [];
- const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
- if (fileIds != null) {
- files = await DriveFiles.createQueryBuilder('file')
- .where('file.userId = :userId AND file.id IN (:...fileIds)', {
- userId: user.id,
- fileIds,
- })
- .orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
- .setParameters({ fileIds })
- .getMany();
- }
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- let renote: Note | null = null;
- if (ps.renoteId != null) {
- // Fetch renote to note
- renote = await Notes.findOneBy({ id: ps.renoteId });
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
- if (renote == null) {
- throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
- throw new ApiError(meta.errors.cannotReRenote);
- }
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
- // Check blocking
- if (renote.userId !== user.id) {
- const block = await Blockings.findOneBy({
- blockerId: renote.userId,
- blockeeId: user.id,
- });
- if (block) {
- throw new ApiError(meta.errors.youHaveBeenBlocked);
+ @Inject(DI.channelsRepository)
+ private channelsRepository: ChannelsRepository,
+
+ private noteEntityService: NoteEntityService,
+ private noteCreateService: NoteCreateService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ let visibleUsers: User[] = [];
+ if (ps.visibleUserIds) {
+ visibleUsers = await this.usersRepository.findBy({
+ id: In(ps.visibleUserIds),
+ });
}
- }
- }
- let reply: Note | null = null;
- if (ps.replyId != null) {
- // Fetch reply
- reply = await Notes.findOneBy({ id: ps.replyId });
+ let files: DriveFile[] = [];
+ const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? 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 (reply == null) {
- throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
- throw new ApiError(meta.errors.cannotReplyToPureRenote);
- }
+ let renote: Note | null = null;
+ if (ps.renoteId != null) {
+ // Fetch renote to note
+ renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
- // Check blocking
- if (reply.userId !== user.id) {
- const block = await Blockings.findOneBy({
- blockerId: reply.userId,
- blockeeId: user.id,
- });
- if (block) {
- throw new ApiError(meta.errors.youHaveBeenBlocked);
+ if (renote == null) {
+ throw new ApiError(meta.errors.noSuchRenoteTarget);
+ } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
+ throw new ApiError(meta.errors.cannotReRenote);
+ }
+
+ // Check blocking
+ if (renote.userId !== me.id) {
+ const block = await this.blockingsRepository.findOneBy({
+ blockerId: renote.userId,
+ blockeeId: me.id,
+ });
+ if (block) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
}
- }
- }
- if (ps.poll) {
- if (typeof ps.poll.expiresAt === 'number') {
- if (ps.poll.expiresAt < Date.now()) {
- throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
+ let reply: Note | 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 (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
+ throw new ApiError(meta.errors.cannotReplyToPureRenote);
+ }
+
+ // Check blocking
+ if (reply.userId !== me.id) {
+ const block = await this.blockingsRepository.findOneBy({
+ blockerId: reply.userId,
+ blockeeId: me.id,
+ });
+ if (block) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
}
- } else if (typeof ps.poll.expiredAfter === 'number') {
- ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
- }
- }
- let channel: Channel | null = null;
- if (ps.channelId != null) {
- channel = await Channels.findOneBy({ id: ps.channelId });
+ if (ps.poll) {
+ if (typeof ps.poll.expiresAt === 'number') {
+ if (ps.poll.expiresAt < Date.now()) {
+ throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
+ }
+ } else if (typeof ps.poll.expiredAfter === 'number') {
+ ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
+ }
+ }
- if (channel == null) {
- throw new ApiError(meta.errors.noSuchChannel);
- }
- }
+ let channel: Channel | null = null;
+ if (ps.channelId != null) {
+ channel = await this.channelsRepository.findOneBy({ id: ps.channelId });
- // 投稿を作成
- const note = await create(user, {
- createdAt: new Date(),
- files: files,
- poll: ps.poll ? {
- choices: ps.poll.choices,
- multiple: ps.poll.multiple || false,
- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
- } : undefined,
- text: ps.text || undefined,
- reply,
- renote,
- cw: ps.cw,
- localOnly: ps.localOnly,
- visibility: ps.visibility,
- visibleUsers,
- channel,
- apMentions: ps.noExtractMentions ? [] : undefined,
- apHashtags: ps.noExtractHashtags ? [] : undefined,
- apEmojis: ps.noExtractEmojis ? [] : undefined,
- });
+ if (channel == null) {
+ throw new ApiError(meta.errors.noSuchChannel);
+ }
+ }
+
+ // 投稿を作成
+ const note = await this.noteCreateService.create(me, {
+ createdAt: new Date(),
+ files: files,
+ poll: ps.poll ? {
+ choices: ps.poll.choices,
+ multiple: ps.poll.multiple || false,
+ expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
+ } : undefined,
+ text: ps.text ?? undefined,
+ reply,
+ renote,
+ cw: ps.cw,
+ localOnly: ps.localOnly,
+ visibility: ps.visibility,
+ visibleUsers,
+ channel,
+ apMentions: ps.noExtractMentions ? [] : undefined,
+ apHashtags: ps.noExtractHashtags ? [] : undefined,
+ apEmojis: ps.noExtractEmojis ? [] : undefined,
+ });
- return {
- createdNote: await Notes.pack(note, user),
- };
-});
+ return {
+ createdNote: await this.noteEntityService.pack(note, me),
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts
index c23ceeb5bf..4769c8bdf1 100644
--- a/packages/backend/src/server/api/endpoints/notes/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/delete.ts
@@ -1,9 +1,11 @@
import ms from 'ms';
-import deleteNote from '@/services/note/delete.js';
-import { Users } from '@/models/index.js';
-import define from '../../define.js';
-import { getNote } from '../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { UsersRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteDeleteService } from '@/core/NoteDeleteService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -42,16 +44,27 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
- if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) {
- throw new ApiError(meta.errors.accessDenied);
- }
+ private getterService: GetterService,
+ private noteDeleteService: NoteDeleteService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ if ((!me.isAdmin && !me.isModerator) && (note.userId !== me.id)) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
- // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
- await deleteNote(await Users.findOneByOrFail({ id: note.userId }), note);
-});
+ // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
+ await this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: note.userId }), note);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
index 097371a425..bfdd1acd22 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -1,8 +1,10 @@
-import { NoteFavorites } from '@/models/index.js';
-import { genId } from '@/misc/gen-id.js';
-import define from '../../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NoteFavoritesRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import { getNote } from '../../../common/getters.js';
export const meta = {
tags: ['notes', 'favorites'],
@@ -35,28 +37,39 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- // Get favoritee
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.noteFavoritesRepository)
+ private noteFavoritesRepository: NoteFavoritesRepository,
- // if already favorited
- const exist = await NoteFavorites.findOneBy({
- noteId: note.id,
- userId: user.id,
- });
+ private idService: IdService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Get favoritee
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- if (exist != null) {
- throw new ApiError(meta.errors.alreadyFavorited);
- }
+ // if already favorited
+ const exist = await this.noteFavoritesRepository.findOneBy({
+ noteId: note.id,
+ userId: me.id,
+ });
+
+ if (exist != null) {
+ throw new ApiError(meta.errors.alreadyFavorited);
+ }
- // Create favorite
- await NoteFavorites.insert({
- id: genId(),
- createdAt: new Date(),
- noteId: note.id,
- userId: user.id,
- });
-});
+ // Create favorite
+ await this.noteFavoritesRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ noteId: note.id,
+ userId: me.id,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
index 82ef4fa197..6b3a02b101 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -1,7 +1,9 @@
-import { NoteFavorites } from '@/models/index.js';
-import define from '../../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { DI } from '@/di-symbols.js';
+import { NoteFavoritesRepository } from '@/models/index.js';
import { ApiError } from '../../../error.js';
-import { getNote } from '../../../common/getters.js';
export const meta = {
tags: ['notes', 'favorites'],
@@ -34,23 +36,33 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- // Get favoritee
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.noteFavoritesRepository)
+ private noteFavoritesRepository: NoteFavoritesRepository,
- // if already favorited
- const exist = await NoteFavorites.findOneBy({
- noteId: note.id,
- userId: user.id,
- });
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Get favoritee
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- if (exist == null) {
- throw new ApiError(meta.errors.notFavorited);
- }
+ // if already favorited
+ const exist = await this.noteFavoritesRepository.findOneBy({
+ noteId: note.id,
+ userId: me.id,
+ });
+
+ if (exist == null) {
+ throw new ApiError(meta.errors.notFavorited);
+ }
- // Delete favorite
- await NoteFavorites.delete(exist.id);
-});
+ // Delete favorite
+ await this.noteFavoritesRepository.delete(exist.id);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index dd9cc581aa..9985f9d257 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -1,7 +1,9 @@
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -29,39 +31,50 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const max = 30;
- const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const query = Notes.createQueryBuilder('note')
- .addSelect('note.score')
- .where('note.userHost IS NULL')
- .andWhere('note.score > 0')
- .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
- .andWhere('note.visibility = \'public\'')
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const max = 30;
+ const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
- if (user) generateMutedUserQuery(query, user);
- if (user) generateBlockedUserQuery(query, user);
+ const query = this.notesRepository.createQueryBuilder('note')
+ .addSelect('note.score')
+ .where('note.userHost IS NULL')
+ .andWhere('note.score > 0')
+ .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
+ .andWhere('note.visibility = \'public\'')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- let notes = await query
- .orderBy('note.score', 'DESC')
- .take(max)
- .getMany();
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
- notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+ let notes = await query
+ .orderBy('note.score', 'DESC')
+ .take(max)
+ .getMany();
- notes = notes.slice(ps.offset, ps.offset + ps.limit);
+ notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
- return await Notes.packMany(notes, user);
-});
+ notes = notes.slice(ps.offset, ps.offset + ps.limit);
+
+ return await this.noteEntityService.packMany(notes, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 925318f544..73b5afa40a 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,13 +1,12 @@
-import { fetchMeta } from '@/misc/fetch-meta.js';
-import { Notes } from '@/models/index.js';
-import { activeUsersChart } from '@/services/chart/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateRepliesQuery } from '../../common/generate-replies-query.js';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
tags: ['notes'],
@@ -49,50 +48,63 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const m = await fetchMeta();
- if (m.disableGlobalTimeline) {
- if (user == null || (!user.isAdmin && !user.isModerator)) {
- throw new ApiError(meta.errors.gtlDisabled);
- }
- }
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- //#region Construct query
- const query = makePaginationQuery(Notes.createQueryBuilder('note'),
- ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
- .andWhere('note.visibility = \'public\'')
- .andWhere('note.channelId IS NULL')
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private metaService: MetaService,
+ private activeUsersChart: ActiveUsersChart,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const m = await this.metaService.fetch();
+ if (m.disableGlobalTimeline) {
+ if (me == null || (!me.isAdmin && !me.isModerator)) {
+ throw new ApiError(meta.errors.gtlDisabled);
+ }
+ }
- generateRepliesQuery(query, user);
- if (user) {
- generateMutedUserQuery(query, user);
- generateMutedNoteQuery(query, user);
- generateBlockedUserQuery(query, user);
- }
+ //#region Construct query
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('note.visibility = \'public\'')
+ .andWhere('note.channelId IS NULL')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
- //#endregion
+ this.queryService.generateRepliesQuery(query, me);
+ if (me) {
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateMutedNoteQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
- const timeline = await query.take(ps.limit).getMany();
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
+ //#endregion
- process.nextTick(() => {
- if (user) {
- activeUsersChart.read(user);
- }
- });
+ const timeline = await query.take(ps.limit).getMany();
- return await Notes.packMany(timeline, user);
-});
+ process.nextTick(() => {
+ if (me) {
+ this.activeUsersChart.read(me);
+ }
+ });
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2dc98c4c9f..c6458223eb 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -1,16 +1,13 @@
import { Brackets } from 'typeorm';
-import { fetchMeta } from '@/misc/fetch-meta.js';
-import { Followings, Notes } from '@/models/index.js';
-import { activeUsersChart } from '@/services/chart/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, FollowingsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { MetaService } from '@/core/MetaService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateRepliesQuery } from '../../common/generate-replies-query.js';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
-import { generateChannelQuery } from '../../common/generate-channel-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
tags: ['notes'],
@@ -57,83 +54,99 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const m = await fetchMeta();
- if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) {
- throw new ApiError(meta.errors.stlDisabled);
- }
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- //#region Construct query
- const followingQuery = Followings.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: user.id });
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
- const query = makePaginationQuery(Notes.createQueryBuilder('note'),
- ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
- .andWhere(new Brackets(qb => {
- qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
- .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
- }))
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
- .setParameters(followingQuery.getParameters());
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private metaService: MetaService,
+ private activeUsersChart: ActiveUsersChart,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const m = await this.metaService.fetch();
+ if (m.disableLocalTimeline && (!me.isAdmin && !me.isModerator)) {
+ throw new ApiError(meta.errors.stlDisabled);
+ }
- generateChannelQuery(query, user);
- generateRepliesQuery(query, user);
- generateVisibilityQuery(query, user);
- generateMutedUserQuery(query, user);
- generateMutedNoteQuery(query, user);
- generateBlockedUserQuery(query, user);
+ //#region Construct query
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- if (ps.includeMyRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.userId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(new Brackets(qb => {
+ qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
+ .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
+ }))
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
+ .setParameters(followingQuery.getParameters());
- if (ps.includeRenotedMyNotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ this.queryService.generateChannelQuery(query, me);
+ this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateVisibilityQuery(query, me);
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateMutedNoteQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
- if (ps.includeLocalRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserHost IS NOT NULL');
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ if (ps.includeMyRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.userId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
- //#endregion
+ if (ps.includeRenotedMyNotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
+
+ if (ps.includeLocalRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserHost IS NOT NULL');
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- const timeline = await query.take(ps.limit).getMany();
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
+ //#endregion
- process.nextTick(() => {
- activeUsersChart.read(user);
- });
+ const timeline = await query.take(ps.limit).getMany();
- return await Notes.packMany(timeline, user);
-});
+ process.nextTick(() => {
+ this.activeUsersChart.read(me);
+ });
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index aac2a3749c..7b8859639d 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,16 +1,14 @@
import { Brackets } from 'typeorm';
-import { fetchMeta } from '@/misc/fetch-meta.js';
-import { Notes, Users } from '@/models/index.js';
-import { activeUsersChart } from '@/services/chart/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import type { UsersRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateRepliesQuery } from '../../common/generate-replies-query.js';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
-import { generateChannelQuery } from '../../common/generate-channel-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
tags: ['notes'],
@@ -56,64 +54,77 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const m = await fetchMeta();
- if (m.disableLocalTimeline) {
- if (user == null || (!user.isAdmin && !user.isModerator)) {
- throw new ApiError(meta.errors.ltlDisabled);
- }
- }
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- //#region Construct query
- const query = makePaginationQuery(Notes.createQueryBuilder('note'),
- ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
- .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private metaService: MetaService,
+ private activeUsersChart: ActiveUsersChart,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const m = await this.metaService.fetch();
+ if (m.disableLocalTimeline) {
+ if (me == null || (!me.isAdmin && !me.isModerator)) {
+ throw new ApiError(meta.errors.ltlDisabled);
+ }
+ }
- generateChannelQuery(query, user);
- generateRepliesQuery(query, user);
- generateVisibilityQuery(query, user);
- if (user) generateMutedUserQuery(query, user);
- if (user) generateMutedNoteQuery(query, user);
- if (user) generateBlockedUserQuery(query, user);
+ //#region Construct query
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
+ this.queryService.generateChannelQuery(query, me);
+ this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateMutedNoteQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
- if (ps.fileType != null) {
- query.andWhere('note.fileIds != \'{}\'');
- query.andWhere(new Brackets(qb => {
- for (const type of ps.fileType!) {
- const i = ps.fileType!.indexOf(type);
- qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
}
- }));
- if (ps.excludeNsfw) {
- query.andWhere('note.cw IS NULL');
- query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
- }
- }
- //#endregion
+ if (ps.fileType != null) {
+ query.andWhere('note.fileIds != \'{}\'');
+ query.andWhere(new Brackets(qb => {
+ for (const type of ps.fileType!) {
+ const i = ps.fileType!.indexOf(type);
+ qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+ }
+ }));
- const timeline = await query.take(ps.limit).getMany();
+ if (ps.excludeNsfw) {
+ query.andWhere('note.cw IS NULL');
+ query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
+ }
+ }
+ //#endregion
+
+ const timeline = await query.take(ps.limit).getMany();
- process.nextTick(() => {
- if (user) {
- activeUsersChart.read(user);
- }
- });
+ process.nextTick(() => {
+ if (me) {
+ this.activeUsersChart.read(me);
+ }
+ });
- return await Notes.packMany(timeline, user);
-});
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
index 9b41544523..9b2dabc88b 100644
--- a/packages/backend/src/server/api/endpoints/notes/mentions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -1,12 +1,12 @@
import { Brackets } from 'typeorm';
-import read from '@/services/note/read.js';
-import { Notes, Followings } from '@/models/index.js';
-import define from '../../define.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
-import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, FollowingsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { NoteReadService } from '@/core/NoteReadService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -37,45 +37,60 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const followingQuery = Followings.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: user.id });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere(new Brackets(qb => { qb
- .where(`'{"${user.id}"}' <@ note.mentions`)
- .orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`);
- }))
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
- generateVisibilityQuery(query, user);
- generateMutedUserQuery(query, user);
- generateMutedNoteThreadQuery(query, user);
- generateBlockedUserQuery(query, user);
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private noteReadService: NoteReadService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- if (ps.visibility) {
- query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
- }
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere(new Brackets(qb => { qb
+ .where(`'{"${me.id}"}' <@ note.mentions`)
+ .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
+ }))
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- if (ps.following) {
- query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
- query.setParameters(followingQuery.getParameters());
- }
+ this.queryService.generateVisibilityQuery(query, me);
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateMutedNoteThreadQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
+
+ if (ps.visibility) {
+ query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
+ }
- const mentions = await query.take(ps.limit).getMany();
+ if (ps.following) {
+ query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id });
+ query.setParameters(followingQuery.getParameters());
+ }
- read(user.id, mentions);
+ const mentions = await query.take(ps.limit).getMany();
- return await Notes.packMany(mentions, user);
-});
+ this.noteReadService.read(me.id, mentions);
+
+ return await this.noteEntityService.packMany(mentions, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
index 5a04d68f3e..11bfdbba0f 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -1,6 +1,9 @@
import { Brackets, In } from 'typeorm';
-import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js';
-import define from '../../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -28,56 +31,75 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const query = Polls.createQueryBuilder('poll')
- .where('poll.userHost IS NULL')
- .andWhere('poll.userId != :meId', { meId: user.id })
- .andWhere('poll.noteVisibility = \'public\'')
- .andWhere(new Brackets(qb => { qb
- .where('poll.expiresAt IS NULL')
- .orWhere('poll.expiresAt > :now', { now: new Date() });
- }));
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- //#region exclude arleady voted polls
- const votedQuery = PollVotes.createQueryBuilder('vote')
- .select('vote.noteId')
- .where('vote.userId = :meId', { meId: user.id });
+ @Inject(DI.pollsRepository)
+ private pollsRepository: PollsRepository,
- query
- .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
+ @Inject(DI.pollVotesRepository)
+ private pollVotesRepository: PollVotesRepository,
- query.setParameters(votedQuery.getParameters());
- //#endregion
+ @Inject(DI.mutingsRepository)
+ private mutingsRepository: MutingsRepository,
- //#region mute
- const mutingQuery = Mutings.createQueryBuilder('muting')
- .select('muting.muteeId')
- .where('muting.muterId = :muterId', { muterId: user.id });
+ private noteEntityService: NoteEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.pollsRepository.createQueryBuilder('poll')
+ .where('poll.userHost IS NULL')
+ .andWhere('poll.userId != :meId', { meId: me.id })
+ .andWhere('poll.noteVisibility = \'public\'')
+ .andWhere(new Brackets(qb => { qb
+ .where('poll.expiresAt IS NULL')
+ .orWhere('poll.expiresAt > :now', { now: new Date() });
+ }));
- query
- .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
+ //#region exclude arleady voted polls
+ const votedQuery = this.pollVotesRepository.createQueryBuilder('vote')
+ .select('vote.noteId')
+ .where('vote.userId = :meId', { meId: me.id });
- query.setParameters(mutingQuery.getParameters());
- //#endregion
+ query
+ .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
- const polls = await query
- .orderBy('poll.noteId', 'DESC')
- .take(ps.limit)
- .skip(ps.offset)
- .getMany();
+ query.setParameters(votedQuery.getParameters());
+ //#endregion
- if (polls.length === 0) return [];
+ //#region mute
+ const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
+ .select('muting.muteeId')
+ .where('muting.muterId = :muterId', { muterId: me.id });
- const notes = await Notes.find({
- where: {
- id: In(polls.map(poll => poll.noteId)),
- },
- order: {
- createdAt: 'DESC',
- },
- });
+ query
+ .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
+
+ query.setParameters(mutingQuery.getParameters());
+ //#endregion
+
+ const polls = await query
+ .orderBy('poll.noteId', 'DESC')
+ .take(ps.limit)
+ .skip(ps.offset)
+ .getMany();
+
+ if (polls.length === 0) return [];
+
+ const notes = await this.notesRepository.find({
+ where: {
+ id: In(polls.map(poll => poll.noteId)),
+ },
+ order: {
+ createdAt: 'DESC',
+ },
+ });
- return await Notes.packMany(notes, user, {
- detail: true,
- });
-});
+ return await this.noteEntityService.packMany(notes, me, {
+ detail: true,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
index 45a832cbd2..76f07528d7 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -1,16 +1,17 @@
import { Not } from 'typeorm';
-import { publishNoteStream } from '@/services/stream.js';
-import { createNotification } from '@/services/create-notification.js';
-import { deliver } from '@/queue/index.js';
-import { renderActivity } from '@/remote/activitypub/renderer/index.js';
-import renderVote from '@/remote/activitypub/renderer/vote.js';
-import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
-import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js';
-import { IRemoteUser } from '@/models/entities/user.js';
-import { genId } from '@/misc/gen-id.js';
-import { getNote } from '../../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
+import type { IRemoteUser } from '@/models/entities/User.js';
+import { IdService } from '@/core/IdService.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { PollService } from '@/core/PollService.js';
+import { ApRendererService } from '@/core/remote/activitypub/ApRendererService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CreateNotificationService } from '@/core/CreateNotificationService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import define from '../../../define.js';
export const meta = {
tags: ['notes'],
@@ -67,103 +68,116 @@ export const paramDef = {
required: ['noteId', 'choice'],
} as const;
+// TODO: ロジックをサービスに切り出す
+
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const createdAt = new Date();
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
- // Get votee
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
- if (!note.hasPoll) {
- throw new ApiError(meta.errors.noPoll);
- }
+ @Inject(DI.pollsRepository)
+ private pollsRepository: PollsRepository,
- // Check blocking
- if (note.userId !== user.id) {
- const block = await Blockings.findOneBy({
- blockerId: note.userId,
- blockeeId: user.id,
- });
- if (block) {
- throw new ApiError(meta.errors.youHaveBeenBlocked);
- }
- }
+ @Inject(DI.pollVotesRepository)
+ private pollVotesRepository: PollVotesRepository,
- const poll = await Polls.findOneByOrFail({ noteId: note.id });
+ private idService: IdService,
+ private getterService: GetterService,
+ private queueService: QueueService,
+ private pollService: PollService,
+ private apRendererService: ApRendererService,
+ private globalEventService: GlobalEventService,
+ private createNotificationService: CreateNotificationService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const createdAt = new Date();
- if (poll.expiresAt && poll.expiresAt < createdAt) {
- throw new ApiError(meta.errors.alreadyExpired);
- }
+ // Get votee
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- if (poll.choices[ps.choice] == null) {
- throw new ApiError(meta.errors.invalidChoice);
- }
+ if (!note.hasPoll) {
+ throw new ApiError(meta.errors.noPoll);
+ }
+
+ // Check blocking
+ if (note.userId !== me.id) {
+ const block = await this.blockingsRepository.findOneBy({
+ blockerId: note.userId,
+ blockeeId: me.id,
+ });
+ if (block) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
- // if already voted
- const exist = await PollVotes.findBy({
- noteId: note.id,
- userId: user.id,
- });
+ const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
- if (exist.length) {
- if (poll.multiple) {
- if (exist.some(x => x.choice === ps.choice)) {
- throw new ApiError(meta.errors.alreadyVoted);
+ if (poll.expiresAt && poll.expiresAt < createdAt) {
+ throw new ApiError(meta.errors.alreadyExpired);
}
- } else {
- throw new ApiError(meta.errors.alreadyVoted);
- }
- }
- // Create vote
- const vote = await PollVotes.insert({
- id: genId(),
- createdAt,
- noteId: note.id,
- userId: user.id,
- choice: ps.choice,
- }).then(x => PollVotes.findOneByOrFail(x.identifiers[0]));
+ if (poll.choices[ps.choice] == null) {
+ throw new ApiError(meta.errors.invalidChoice);
+ }
- // Increment votes count
- const index = ps.choice + 1; // In SQL, array index is 1 based
- await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
+ // if already voted
+ const exist = await this.pollVotesRepository.findBy({
+ noteId: note.id,
+ userId: me.id,
+ });
+
+ if (exist.length) {
+ if (poll.multiple) {
+ if (exist.some(x => x.choice === ps.choice)) {
+ throw new ApiError(meta.errors.alreadyVoted);
+ }
+ } else {
+ throw new ApiError(meta.errors.alreadyVoted);
+ }
+ }
+
+ // Create vote
+ const vote = await this.pollVotesRepository.insert({
+ id: this.idService.genId(),
+ createdAt,
+ noteId: note.id,
+ userId: me.id,
+ choice: ps.choice,
+ }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0]));
- publishNoteStream(note.id, 'pollVoted', {
- choice: ps.choice,
- userId: user.id,
- });
+ // Increment votes count
+ const index = ps.choice + 1; // In SQL, array index is 1 based
+ await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
- // Notify
- createNotification(note.userId, 'pollVote', {
- notifierId: user.id,
- noteId: note.id,
- choice: ps.choice,
- });
+ this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
+ choice: ps.choice,
+ userId: me.id,
+ });
- // Fetch watchers
- NoteWatchings.findBy({
- noteId: note.id,
- userId: Not(user.id),
- }).then(watchers => {
- for (const watcher of watchers) {
- createNotification(watcher.userId, 'pollVote', {
- notifierId: user.id,
+ // Notify
+ this.createNotificationService.createNotification(note.userId, 'pollVote', {
+ notifierId: me.id,
noteId: note.id,
choice: ps.choice,
});
- }
- });
- // リモート投票の場合リプライ送信
- if (note.userHost != null) {
- const pollOwner = await Users.findOneByOrFail({ id: note.userId }) as IRemoteUser;
+ // リモート投票の場合リプライ送信
+ if (note.userHost != null) {
+ const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
- deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
- }
+ this.queueService.deliver(me, this.apRendererService.renderActivity(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox);
+ }
- // リモートフォロワーにUpdate配信
- deliverQuestionUpdate(note.id);
-});
+ // リモートフォロワーにUpdate配信
+ this.pollService.deliverQuestionUpdate(note.id);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index 15a62d394d..d57950f012 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -1,8 +1,12 @@
-import { DeepPartial, FindOptionsWhere } from 'typeorm';
-import { NoteReactions } from '@/models/index.js';
-import { NoteReaction } from '@/models/entities/note-reaction.js';
-import define from '../../define.js';
+import { DeepPartial } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
+import { NoteReactionsRepository } from '@/models/index.js';
+import type { NoteReaction } from '@/models/entities/NoteReaction.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import type { FindOptionsWhere } from 'typeorm';
export const meta = {
tags: ['notes', 'reactions'],
@@ -45,28 +49,38 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const query = {
- noteId: ps.noteId,
- } as FindOptionsWhere<NoteReaction>;
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.noteReactionsRepository)
+ private noteReactionsRepository: NoteReactionsRepository,
- if (ps.type) {
- // ローカルリアクションはホスト名が . とされているが
- // DB 上ではそうではないので、必要に応じて変換
- const suffix = '@.:';
- const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
- query.reaction = type;
- }
+ private noteReactionEntityService: NoteReactionEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = {
+ noteId: ps.noteId,
+ } as FindOptionsWhere<NoteReaction>;
- const reactions = await NoteReactions.find({
- where: query,
- take: ps.limit,
- skip: ps.offset,
- order: {
- id: -1,
- },
- relations: ['user', 'user.avatar', 'user.banner', 'note'],
- });
+ if (ps.type) {
+ // ローカルリアクションはホスト名が . とされているが
+ // DB 上ではそうではないので、必要に応じて変換
+ const suffix = '@.:';
+ const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
+ query.reaction = type;
+ }
- return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
-});
+ const reactions = await this.noteReactionsRepository.find({
+ where: query,
+ take: ps.limit,
+ skip: ps.offset,
+ order: {
+ id: -1,
+ },
+ relations: ['user', 'user.avatar', 'user.banner', 'note'],
+ });
+
+ return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index 07e52a9266..2af734307d 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -1,6 +1,7 @@
-import createReaction from '@/services/note/reaction/create.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { ReactionService } from '@/core/ReactionService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -41,15 +42,23 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
- await createReaction(user, note, ps.reaction).catch(e => {
- if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
- if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
- throw e;
- });
- return;
-});
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ private getterService: GetterService,
+ private reactionService: ReactionService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+ await this.reactionService.create(me, note, ps.reaction).catch(err => {
+ if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
+ if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
+ throw err;
+ });
+ return;
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
index c13cafa21d..31ed962922 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
@@ -1,7 +1,8 @@
import ms from 'ms';
-import deleteReaction from '@/services/note/reaction/delete.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { ReactionService } from '@/core/ReactionService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -41,13 +42,21 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
- await deleteReaction(user, note).catch(e => {
- if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
- throw e;
- });
-});
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ private getterService: GetterService,
+ private reactionService: ReactionService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+ await this.reactionService.delete(me, note).catch(err => {
+ if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
+ throw err;
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index 28be360763..57b7aeae0d 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -1,11 +1,11 @@
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
-import { getNote } from '../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -43,31 +43,43 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere('note.renoteId = :renoteId', { renoteId: note.id })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- generateVisibilityQuery(query, user);
- if (user) generateMutedUserQuery(query, user);
- if (user) generateBlockedUserQuery(query, user);
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere('note.renoteId = :renoteId', { renoteId: note.id })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- const renotes = await query.take(ps.limit).getMany();
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
- return await Notes.packMany(renotes, user);
-});
+ const renotes = await query.take(ps.limit).getMany();
+
+ return await this.noteEntityService.packMany(renotes, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index ab0018f58e..7020d0c681 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -1,9 +1,9 @@
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -33,26 +33,37 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere('note.replyId = :replyId', { replyId: ps.noteId })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- generateVisibilityQuery(query, user);
- if (user) generateMutedUserQuery(query, user);
- if (user) generateBlockedUserQuery(query, user);
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere('note.replyId = :replyId', { replyId: ps.noteId })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- const timeline = await query.take(ps.limit).getMany();
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
- return await Notes.packMany(timeline, user);
-});
+ const timeline = await query.take(ps.limit).getMany();
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 777de7221c..0727c9af6c 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -1,12 +1,12 @@
import { Brackets } from 'typeorm';
-import { Notes } from '@/models/index.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import define from '../../define.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes', 'hashtags'],
@@ -66,75 +66,86 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, me) => {
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- generateVisibilityQuery(query, me);
- if (me) generateMutedUserQuery(query, me);
- if (me) generateBlockedUserQuery(query, me);
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- try {
- if (ps.tag) {
- if (!safeForSql(ps.tag)) throw 'Injection';
- query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
- } else {
- query.andWhere(new Brackets(qb => {
- for (const tags of ps.query!) {
- qb.orWhere(new Brackets(qb => {
- for (const tag of tags) {
- if (!safeForSql(tag)) throw 'Injection';
- qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
+
+ try {
+ if (ps.tag) {
+ if (!safeForSql(ps.tag)) throw 'Injection';
+ query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
+ } else {
+ query.andWhere(new Brackets(qb => {
+ for (const tags of ps.query!) {
+ qb.orWhere(new Brackets(qb => {
+ for (const tag of tags) {
+ if (!safeForSql(tag)) throw 'Injection';
+ qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
+ }
+ }));
}
}));
}
- }));
- }
- } catch (e) {
- if (e === 'Injection') return [];
- throw e;
- }
+ } catch (e) {
+ if (e === 'Injection') return [];
+ throw e;
+ }
- if (ps.reply != null) {
- if (ps.reply) {
- query.andWhere('note.replyId IS NOT NULL');
- } else {
- query.andWhere('note.replyId IS NULL');
- }
- }
+ if (ps.reply != null) {
+ if (ps.reply) {
+ query.andWhere('note.replyId IS NOT NULL');
+ } else {
+ query.andWhere('note.replyId IS NULL');
+ }
+ }
- if (ps.renote != null) {
- if (ps.renote) {
- query.andWhere('note.renoteId IS NOT NULL');
- } else {
- query.andWhere('note.renoteId IS NULL');
- }
- }
+ if (ps.renote != null) {
+ if (ps.renote) {
+ query.andWhere('note.renoteId IS NOT NULL');
+ } else {
+ query.andWhere('note.renoteId IS NULL');
+ }
+ }
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
- if (ps.poll != null) {
- if (ps.poll) {
- query.andWhere('note.hasPoll = TRUE');
- } else {
- query.andWhere('note.hasPoll = FALSE');
- }
- }
+ if (ps.poll != null) {
+ if (ps.poll) {
+ query.andWhere('note.hasPoll = TRUE');
+ } else {
+ query.andWhere('note.hasPoll = FALSE');
+ }
+ }
- // Search notes
- const notes = await query.take(ps.limit).getMany();
+ // Search notes
+ const notes = await query.take(ps.limit).getMany();
- return await Notes.packMany(notes, me);
-});
+ return await this.noteEntityService.packMany(notes, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index 4e2cdae801..484cfc1128 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -1,12 +1,11 @@
import { In } from 'typeorm';
-import { Notes } from '@/models/index.js';
-import config from '@/config/index.js';
-import es from '../../../../db/elasticsearch.js';
-import define from '../../define.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { Config } from '@/config.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -46,97 +45,51 @@ export const paramDef = {
required: ['query'],
} as const;
-// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, me) => {
- if (es == null) {
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId);
-
- if (ps.userId) {
- query.andWhere('note.userId = :userId', { userId: ps.userId });
- } else if (ps.channelId) {
- query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
- }
-
- query
- .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
-
- generateVisibilityQuery(query, me);
- if (me) generateMutedUserQuery(query, me);
- if (me) generateBlockedUserQuery(query, me);
+// TODO: ロジックをサービスに切り出す
- const notes = await query.take(ps.limit).getMany();
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- return await Notes.packMany(notes, me);
- } else {
- const userQuery = ps.userId != null ? [{
- term: {
- userId: ps.userId,
- },
- }] : [];
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
- const hostQuery = ps.userId == null ?
- ps.host === null ? [{
- bool: {
- must_not: {
- exists: {
- field: 'userHost',
- },
- },
- },
- }] : ps.host !== undefined ? [{
- term: {
- userHost: ps.host,
- },
- }] : []
- : [];
+ if (ps.userId) {
+ query.andWhere('note.userId = :userId', { userId: ps.userId });
+ } else if (ps.channelId) {
+ query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
+ }
- const result = await es.search({
- index: config.elasticsearch.index || 'misskey_note',
- body: {
- size: ps.limit,
- from: ps.offset,
- query: {
- bool: {
- must: [{
- simple_query_string: {
- fields: ['text'],
- query: ps.query.toLowerCase(),
- default_operator: 'and',
- },
- }, ...hostQuery, ...userQuery],
- },
- },
- sort: [{
- _doc: 'desc',
- }],
- },
- });
+ query
+ .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
- const hits = result.body.hits.hits.map((hit: any) => hit._id);
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
- if (hits.length === 0) return [];
+ const notes = await query.take(ps.limit).getMany();
- // Fetch found notes
- const notes = await Notes.find({
- where: {
- id: In(hits),
- },
- order: {
- id: -1,
- },
+ return await this.noteEntityService.packMany(notes, me);
});
-
- return await Notes.packMany(notes, me);
}
-});
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index 5cd74bd2ca..c3f5b9dfb0 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -1,7 +1,10 @@
-import { Notes } from '@/models/index.js';
-import define from '../../define.js';
-import { getNote } from '../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -32,13 +35,24 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- return await Notes.pack(note, user, {
- detail: true,
- });
-});
+ private noteEntityService: NoteEntityService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ return await this.noteEntityService.pack(note, me, {
+ detail: true,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts
index 01afa5add2..7756d39f7c 100644
--- a/packages/backend/src/server/api/endpoints/notes/state.ts
+++ b/packages/backend/src/server/api/endpoints/notes/state.ts
@@ -1,5 +1,7 @@
-import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -35,36 +37,42 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await Notes.findOneByOrFail({ id: ps.noteId });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const [favorite, watching, threadMuting] = await Promise.all([
- NoteFavorites.count({
- where: {
- userId: user.id,
- noteId: note.id,
- },
- take: 1,
- }),
- NoteWatchings.count({
- where: {
- userId: user.id,
- noteId: note.id,
- },
- take: 1,
- }),
- NoteThreadMutings.count({
- where: {
- userId: user.id,
- threadId: note.threadId || note.id,
- },
- take: 1,
- }),
- ]);
+ @Inject(DI.noteThreadMutingsRepository)
+ private noteThreadMutingsRepository: NoteThreadMutingsRepository,
+
+ @Inject(DI.noteFavoritesRepository)
+ private noteFavoritesRepository: NoteFavoritesRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId });
+
+ const [favorite, threadMuting] = await Promise.all([
+ this.noteFavoritesRepository.count({
+ where: {
+ userId: me.id,
+ noteId: note.id,
+ },
+ take: 1,
+ }),
+ this.noteThreadMutingsRepository.count({
+ where: {
+ userId: me.id,
+ threadId: note.threadId || note.id,
+ },
+ take: 1,
+ }),
+ ]);
- return {
- isFavorited: favorite !== 0,
- isWatching: watching !== 0,
- isMutedThread: threadMuting !== 0,
- };
-});
+ return {
+ isFavorited: favorite !== 0,
+ isMutedThread: threadMuting !== 0,
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
index cf360526d3..1c83adddff 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -1,8 +1,10 @@
-import { Notes, NoteThreadMutings } from '@/models/index.js';
-import { genId } from '@/misc/gen-id.js';
-import readNote from '@/services/note/read.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js';
+import { IdService } from '@/core/IdService.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { NoteReadService } from '@/core/NoteReadService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -30,26 +32,41 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- const mutedNotes = await Notes.find({
- where: [{
- id: note.threadId || note.id,
- }, {
- threadId: note.threadId || note.id,
- }],
- });
+ @Inject(DI.noteThreadMutingsRepository)
+ private noteThreadMutingsRepository: NoteThreadMutingsRepository,
- await readNote(user.id, mutedNotes);
+ private getterService: GetterService,
+ private noteReadService: NoteReadService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- await NoteThreadMutings.insert({
- id: genId(),
- createdAt: new Date(),
- threadId: note.threadId || note.id,
- userId: user.id,
- });
-});
+ const mutedNotes = await this.notesRepository.find({
+ where: [{
+ id: note.threadId || note.id,
+ }, {
+ threadId: note.threadId || note.id,
+ }],
+ });
+
+ await this.noteReadService.read(me.id, mutedNotes);
+
+ await this.noteThreadMutingsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ threadId: note.threadId || note.id,
+ userId: me.id,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
index ac310d0fe6..1f896734d1 100644
--- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
@@ -1,6 +1,8 @@
-import { NoteThreadMutings } from '@/models/index.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NoteThreadMutingsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { GetterService } from '@/server/api/common/GetterService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -28,14 +30,24 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.noteThreadMutingsRepository)
+ private noteThreadMutingsRepository: NoteThreadMutingsRepository,
- await NoteThreadMutings.delete({
- threadId: note.threadId || note.id,
- userId: user.id,
- });
-});
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ await this.noteThreadMutingsRepository.delete({
+ threadId: note.threadId || note.id,
+ userId: me.id,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 22f4925175..53a1ae1348 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -1,14 +1,12 @@
import { Brackets } from 'typeorm';
-import { Notes, Followings } from '@/models/index.js';
-import { activeUsersChart } from '@/services/chart/index.js';
-import define from '../../define.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
-import { generateRepliesQuery } from '../../common/generate-replies-query.js';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
-import { generateChannelQuery } from '../../common/generate-channel-query.js';
-import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, FollowingsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -47,85 +45,100 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const hasFollowing = (await Followings.count({
- where: {
- followerId: user.id,
- },
- take: 1,
- })) !== 0;
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- //#region Construct query
- const followingQuery = Followings.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: user.id });
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
- const query = makePaginationQuery(Notes.createQueryBuilder('note'),
- ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
- .andWhere(new Brackets(qb => { qb
- .where('note.userId = :meId', { meId: user.id });
- if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
- }))
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
- .setParameters(followingQuery.getParameters());
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private activeUsersChart: ActiveUsersChart,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const hasFollowing = (await this.followingsRepository.count({
+ where: {
+ followerId: me.id,
+ },
+ take: 1,
+ })) !== 0;
- generateChannelQuery(query, user);
- generateRepliesQuery(query, user);
- generateVisibilityQuery(query, user);
- generateMutedUserQuery(query, user);
- generateMutedNoteQuery(query, user);
- generateBlockedUserQuery(query, user);
+ //#region Construct query
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- if (ps.includeMyRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.userId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(new Brackets(qb => { qb
+ .where('note.userId = :meId', { meId: me.id });
+ if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
+ }))
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
+ .setParameters(followingQuery.getParameters());
- if (ps.includeRenotedMyNotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ this.queryService.generateChannelQuery(query, me);
+ this.queryService.generateRepliesQuery(query, me);
+ this.queryService.generateVisibilityQuery(query, me);
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateMutedNoteQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
- if (ps.includeLocalRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserHost IS NOT NULL');
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ if (ps.includeMyRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.userId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
- //#endregion
+ if (ps.includeRenotedMyNotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
+
+ if (ps.includeLocalRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserHost IS NOT NULL');
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- const timeline = await query.take(ps.limit).getMany();
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
+ //#endregion
- process.nextTick(() => {
- activeUsersChart.read(user);
- });
+ const timeline = await query.take(ps.limit).getMany();
- return await Notes.packMany(timeline, user);
-});
+ process.nextTick(() => {
+ this.activeUsersChart.read(me);
+ });
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 5e40e7106f..c24f1e401e 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -1,12 +1,15 @@
import { URLSearchParams } from 'node:url';
import fetch from 'node-fetch';
-import config from '@/config/index.js';
-import { getAgentByUrl } from '@/misc/fetch.js';
-import { fetchMeta } from '@/misc/fetch-meta.js';
-import { Notes } from '@/models/index.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { Config } from '@/config.js';
+import { DI } from '@/di-symbols.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { MetaService } from '@/core/MetaService.js';
+import { HttpRequestService } from '@/core/HttpRequestService.js';
import { ApiError } from '../../error.js';
-import { getNote } from '../../common/getters.js';
-import define from '../../define.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -37,58 +40,74 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) {
- return 204; // TODO: 良い感じのエラー返す
- }
+ private noteEntityService: NoteEntityService,
+ private getterService: GetterService,
+ private metaService: MetaService,
+ private httpRequestService: HttpRequestService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
- if (note.text == null) {
- return 204;
- }
+ if (!(await this.noteEntityService.isVisibleForMe(note, me ? me.id : null))) {
+ return 204; // TODO: 良い感じのエラー返す
+ }
- const instance = await fetchMeta();
+ if (note.text == null) {
+ return 204;
+ }
- if (instance.deeplAuthKey == null) {
- return 204; // TODO: 良い感じのエラー返す
- }
+ const instance = await this.metaService.fetch();
- let targetLang = ps.targetLang;
- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
+ if (instance.deeplAuthKey == null) {
+ return 204; // TODO: 良い感じのエラー返す
+ }
- const params = new URLSearchParams();
- params.append('auth_key', instance.deeplAuthKey);
- params.append('text', note.text);
- params.append('target_lang', targetLang);
+ let targetLang = ps.targetLang;
+ if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
- const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+ const params = new URLSearchParams();
+ params.append('auth_key', instance.deeplAuthKey);
+ params.append('text', note.text);
+ params.append('target_lang', targetLang);
- const res = await fetch(endpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'User-Agent': config.userAgent,
- Accept: 'application/json, */*',
- },
- body: params,
- // TODO
- //timeout: 10000,
- agent: getAgentByUrl,
- });
+ const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
- const json = (await res.json()) as {
+ const res = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'User-Agent': config.userAgent,
+ Accept: 'application/json, */*',
+ },
+ body: params,
+ // TODO
+ //timeout: 10000,
+ agent: (url) => this.httpRequestService.getAgentByUrl(url),
+ });
+
+ const json = (await res.json()) as {
translations: {
detected_source_language: string;
text: string;
}[];
};
- return {
- sourceLang: json.translations[0].detected_source_language,
- text: json.translations[0].text,
- };
-});
+ return {
+ sourceLang: json.translations[0].detected_source_language,
+ text: json.translations[0].text,
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
index 3fba0efe0c..c0048888b4 100644
--- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
@@ -1,9 +1,11 @@
import ms from 'ms';
-import deleteNote from '@/services/note/delete.js';
-import { Notes, Users } from '@/models/index.js';
-import define from '../../define.js';
-import { getNote } from '../../common/getters.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { UsersRepository, NotesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteDeleteService } from '@/core/NoteDeleteService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { GetterService } from '../../common/GetterService.js';
export const meta = {
tags: ['notes'],
@@ -36,18 +38,32 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
- const renotes = await Notes.findBy({
- userId: user.id,
- renoteId: note.id,
- });
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- for (const note of renotes) {
- deleteNote(await Users.findOneByOrFail({ id: user.id }), note);
+ private getterService: GetterService,
+ private noteDeleteService: NoteDeleteService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ const renotes = await this.notesRepository.findBy({
+ userId: me.id,
+ renoteId: note.id,
+ });
+
+ for (const note of renotes) {
+ this.noteDeleteService.delete(await this.usersRepository.findOneByOrFail({ id: me.id }), note);
+ }
+ });
}
-});
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index e603a8f625..87a464578c 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -1,10 +1,12 @@
import { Brackets } from 'typeorm';
-import { UserLists, UserListJoinings, Notes } from '@/models/index.js';
-import { activeUsersChart } from '@/services/chart/index.js';
-import define from '../../define.js';
+import { Inject, Injectable } from '@nestjs/common';
+import { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import { makePaginationQuery } from '../../common/make-pagination-query.js';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
export const meta = {
tags: ['notes', 'lists'],
@@ -52,72 +54,90 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const list = await UserLists.findOneBy({
- id: ps.listId,
- userId: user.id,
- });
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
- if (list == null) {
- throw new ApiError(meta.errors.noSuchList);
- }
+ @Inject(DI.userListsRepository)
+ private userListsRepository: UserListsRepository,
- //#region Construct query
- const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .innerJoin(UserListJoinings.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
- .andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
- generateVisibilityQuery(query, user);
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private activeUsersChart: ActiveUsersChart,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const list = await this.userListsRepository.findOneBy({
+ id: ps.listId,
+ userId: me.id,
+ });
- if (ps.includeMyRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.userId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ if (list == null) {
+ throw new ApiError(meta.errors.noSuchList);
+ }
- if (ps.includeRenotedMyNotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ //#region Construct query
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('user.avatar', 'avatar')
+ .leftJoinAndSelect('user.banner', 'banner')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
+ .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
+ .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
+ .andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
- if (ps.includeLocalRenotes === false) {
- query.andWhere(new Brackets(qb => {
- qb.orWhere('note.renoteUserHost IS NOT NULL');
- qb.orWhere('note.renoteId IS NULL');
- qb.orWhere('note.text IS NOT NULL');
- qb.orWhere('note.fileIds != \'{}\'');
- qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
- }));
- }
+ this.queryService.generateVisibilityQuery(query, me);
- if (ps.withFiles) {
- query.andWhere('note.fileIds != \'{}\'');
- }
- //#endregion
+ if (ps.includeMyRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.userId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- const timeline = await query.take(ps.limit).getMany();
+ if (ps.includeRenotedMyNotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- activeUsersChart.read(user);
+ if (ps.includeLocalRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserHost IS NOT NULL');
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- return await Notes.packMany(timeline, user);
-});
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
+ //#endregion
+
+ const timeline = await query.take(ps.limit).getMany();
+
+ this.activeUsersChart.read(me);
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts
deleted file mode 100644
index 7d482b0732..0000000000
--- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import watch from '@/services/note/watch.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
-import { ApiError } from '../../../error.js';
-
-export const meta = {
- tags: ['notes'],
-
- requireCredential: true,
-
- kind: 'write:account',
-
- errors: {
- noSuchNote: {
- message: 'No such note.',
- code: 'NO_SUCH_NOTE',
- id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7',
- },
- },
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- noteId: { type: 'string', format: 'misskey:id' },
- },
- required: ['noteId'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
-
- await watch(user.id, note);
-});
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
deleted file mode 100644
index 2c1a2e5fbd..0000000000
--- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import unwatch from '@/services/note/unwatch.js';
-import define from '../../../define.js';
-import { getNote } from '../../../common/getters.js';
-import { ApiError } from '../../../error.js';
-
-export const meta = {
- tags: ['notes'],
-
- requireCredential: true,
-
- kind: 'write:account',
-
- errors: {
- noSuchNote: {
- message: 'No such note.',
- code: 'NO_SUCH_NOTE',
- id: '09b3695c-f72c-4731-a428-7cff825fc82e',
- },
- },
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- noteId: { type: 'string', format: 'misskey:id' },
- },
- required: ['noteId'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, user) => {
- const note = await getNote(ps.noteId).catch(e => {
- if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
- throw e;
- });
-
- await unwatch(user.id, note);
-});