summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2026-03-05 10:56:50 +0000
committerGitHub <noreply@github.com>2026-03-05 10:56:50 +0000
commitfe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch)
treeaf6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/backend/src/server/api/endpoints
parentMerge pull request #16998 from misskey-dev/develop (diff)
parentRelease: 2026.3.0 (diff)
downloadmisskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/list.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-user-ips.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/ap/get.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/hashtags/users.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/change-password.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/delete-account.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications-grouped.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/update-email.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts167
25 files changed, 222 insertions, 100 deletions
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index b8bfda73a4..74462b302a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
- const { raw, packed } = await this.announcementService.create({
+ const { packed } = await this.announcementService.create({
updatedAt: null,
title: ps.title,
text: ps.text,
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
index 804bd5d9b9..aeebceed5a 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -51,11 +51,13 @@ export const meta = {
},
icon: {
type: 'string',
- optional: false, nullable: true,
+ optional: false, nullable: false,
+ enum: ['info', 'warning', 'error', 'success'],
},
display: {
type: 'string',
optional: false, nullable: false,
+ enum: ['normal', 'banner', 'dialog'],
},
isActive: {
type: 'boolean',
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index cf03859ce5..d4305e7d7c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
// Create file
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
- } catch (e) {
+ } catch (_) {
// TODO: need to return Drive Error
throw new ApiError();
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 660aa55bf8..b9448b4bc2 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 34d200455e..658367409c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 7bde10af46..e20bc21f6b 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
- const mustBeNever: never = error;
+ const _mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
index b7781b8c99..bdd0ee6cac 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
kind: 'read:admin:user-ips',
res: {
type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 2c7f793584..5beed3a7e8 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -428,8 +428,7 @@ export const meta = {
optional: false, nullable: true,
},
clientOptions: {
- type: 'object',
- optional: false, nullable: false,
+ ref: 'MetaClientOptions',
},
description: {
type: 'string',
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index b3c2cecc67..372fe3a25f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Injectable, Inject } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -67,7 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
- clientOptions: { type: 'object', nullable: false },
+ clientOptions: {
+ type: 'object', nullable: false,
+ properties: {
+ entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
+ showTimelineForVisitor: { type: 'boolean', nullable: false },
+ showActivitiesForVisitor: { type: 'boolean', nullable: false },
+ },
+ },
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@@ -217,6 +225,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private metaService: MetaService,
private moderationLogService: ModerationLogService,
) {
@@ -329,7 +340,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.clientOptions !== undefined) {
- set.clientOptions = ps.clientOptions;
+ set.clientOptions = {
+ ...this.serverSettings.clientOptions,
+ ...ps.clientOptions,
+ };
}
if (ps.cacheRemoteFiles !== undefined) {
diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts
index 14286bc23e..ff03fce72b 100644
--- a/packages/backend/src/server/api/endpoints/ap/get.ts
+++ b/packages/backend/src/server/api/endpoints/ap/get.ts
@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apResolverService: ApResolverService,
) {
super(meta, paramDef, async (ps, me) => {
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(ps.uri);
return object;
});
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index fe48e7497a..47da6b4fbd 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ
- const resolver = this.apResolverService.createResolver();
+ const resolver = await this.apResolverService.createResolver();
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
if (err instanceof IdentifiableError) {
@@ -215,7 +215,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
type: 'Note',
object,
};
- } catch (e) {
+ } catch (_) {
return null;
}
}
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index 30f0c1b0c8..7b2c137bd4 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
tag: { type: 'string' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
@@ -74,7 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
}
- const users = await query.limit(ps.limit).getMany();
+ const users = await query
+ .limit(ps.limit)
+ .offset(ps.offset)
+ .getMany();
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 65eece5b97..8dc5cafb56 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 9391aee5e0..050dbaf49e 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -212,7 +212,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a54c598213..b6c837eda7 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index c350136eae..6e5d9943de 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index b5a53cc889..23b577dc18 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -57,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index bb78d47149..19ea187ee8 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index bfa0b4605d..42324c7778 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index f933eaab00..4fe39bb8e8 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -71,7 +71,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
- const EXTRA_LIMIT = 100;
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : undefined);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : undefined);
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index da1faee30d..c2f4281f36 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
- } catch (e) {
+ } catch (_) {
throw new Error('authentication failed');
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 9971a1ea4d..5207d9f2b0 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -323,7 +323,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
new RE2(regexp[1], regexp[2]);
- } catch (err) {
+ } catch (_) {
throw new ApiError(meta.errors.invalidRegexp);
}
}
@@ -587,7 +587,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
})
.execute();
}
- } catch (err) {
+ } catch (_) {
// なにもしない
}
}
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 29c6aa7434..7c0dddb827 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
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
- const mutedNotes = await this.notesRepository.find({
+ const _mutedNotes = await this.notesRepository.find({
where: [{
id: note.threadId ?? note.id,
}, {
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 047f9a053b..4defcc9dcf 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -86,7 +86,7 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- birthday: { ...birthdaySchema, nullable: true },
+ birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-users-by-birthday instead.' },
},
},
],
@@ -146,15 +146,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee');
+ // @deprecated use get-following-users-by-birthday instead.
if (ps.birthday) {
- try {
- const birthday = ps.birthday.substring(5, 10);
- const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
- birthdayUserQuery.select('user_profile.userId')
- .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
+ query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
- query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
- } catch (err) {
+ try {
+ const birthday = ps.birthday.split('-');
+ birthday.shift(); // 年の部分を削除
+ // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
+ } catch (_) {
throw new ApiError(meta.errors.birthdayInvalid);
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts
new file mode 100644
index 0000000000..947c19d81e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts
@@ -0,0 +1,167 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type {
+ FollowingsRepository,
+ UserProfilesRepository,
+} from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type { Packed } from '@/misc/json-schema.js';
+
+export const meta = {
+ tags: ['users'],
+
+ requireCredential: true,
+ kind: 'read:account',
+
+ description: 'Retrieve users who have a birthday on the specified range.',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'misskey:id',
+ },
+ birthday: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', default: 0 },
+ birthday: {
+ oneOf: [{
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ }, {
+ type: 'object',
+ properties: {
+ begin: {
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ },
+ end: {
+ type: 'object',
+ properties: {
+ month: { type: 'integer', minimum: 1, maximum: 12 },
+ day: { type: 'integer', minimum: 1, maximum: 31 },
+ },
+ required: ['month', 'day'],
+ },
+ },
+ required: ['begin', 'end'],
+ }],
+ },
+ },
+ required: ['birthday'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.followingsRepository)
+ private followingsRepository: FollowingsRepository,
+
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.followingsRepository
+ .createQueryBuilder('following')
+ .andWhere('following.followerId = :userId', { userId: me.id })
+ .innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
+
+ if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
+ const range = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
+
+ // 誕生日は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)でインデックスが効くようになっているので、その形式に変換
+ const begin = range.begin.month * 100 + range.begin.day;
+ const end = range.end.month * 100 + range.end.day;
+
+ if (begin <= end) {
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin, end });
+ } else {
+ // 12/31 から 1/1 の範囲を取得するために OR で対応
+ query.andWhere(new Brackets(qb => {
+ qb.where('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND 1231', { begin });
+ qb.orWhere('get_birthday_date(followeeProfile.birthday) BETWEEN 101 AND :end', { end });
+ }));
+ }
+ } else {
+ const { month, day } = ps.birthday as { month: number; day: number };
+ // なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
+ query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
+ }
+
+ query.select('following.followeeId', 'user_id');
+ query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
+ query.orderBy('birthday_date', 'ASC');
+
+ const birthdayUsers = await query
+ .offset(ps.offset).limit(ps.limit)
+ .getRawMany<{ birthday_date: number; user_id: string }>();
+
+ const users = new Map<string, Packed<'UserLite'>>((
+ await this.userEntityService.packMany(
+ birthdayUsers.map(u => u.user_id),
+ me,
+ { schema: 'UserLite' },
+ )
+ ).map(u => [u.id, u]));
+
+ return birthdayUsers
+ .map(item => {
+ const birthday = new Date();
+ birthday.setHours(0, 0, 0, 0);
+ // item.birthday_date は mmdd の形式の最大4桁の数字(例: 8月30日 → 830)で出力されるので、日付に戻してDateオブジェクトに設定
+ birthday.setMonth(Math.floor(item.birthday_date / 100) - 1, item.birthday_date % 100);
+
+ if (birthday.getTime() < new Date().setHours(0, 0, 0, 0)) {
+ birthday.setFullYear(new Date().getFullYear() + 1);
+ }
+
+ const birthdayStr = `${birthday.getFullYear()}-${(birthday.getMonth() + 1).toString().padStart(2, '0')}-${(birthday.getDate()).toString().padStart(2, '0')}`;
+ return {
+ id: item.user_id,
+ birthday: birthdayStr,
+ user: users.get(item.user_id),
+ };
+ })
+ .filter(item => item.user != null)
+ .map(item => item as { id: string; birthday: string; user: Packed<'UserLite'> });
+ });
+ }
+}