From 16ffd88ecc16c394fd8bc532c61e6be45b053ca5 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:33:26 +0900 Subject: enhance: 誕生日のユーザーウィジェットで、今日だけに限らず、直近の誕生日ユーザーを表示できるように (#13637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットをリファクタリング (cherry picked from commit 24652b9364fd7d898ac176e0da3bb6e957f72328) * fix(backend): 年越しの時期で誕生日検索クエリーが誤動作する問題を修正 (MisskeyIO#577) (cherry picked from commit 38581006be35b4725b235be9f178d2c0c9c5af33) * fix * spdx * delete birthday param on users/following api * 名称を一本化 * Update Changelog * Update Changelog * fix(frontend/WidgetBirthdayFollowings): ユーザーの名前が長いと投稿ボタンがはみ出てしまう問題を修正 (MisskeyIO#582) (cherry picked from commit fa47a545b1b770e5f3e52bb2798d1104da5dd244) * use module css * default 3day * Revert "delete birthday param on users/following api" This reverts commit a47456c1c43410409ed3607ae1d6ebb69a82324b. * Update Changelog * 日付が1ヶ月ズレている問題を修正? * fix: 日付関連のバグを修正 Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * build misskey-js types * add comment * Update CHANGELOG.md * migrate * change migration * UPdate Changelog * fix: revert unnecessary changes * :art: * i18n * fix * update changelog * :art: * fix lint * refactor: remove unnecessary classes * fix * fix --------- Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> --- .../src/server/api/endpoints/users/following.ts | 15 +- .../users/get-following-birthday-users.ts | 167 +++++++++++++++++++++ 2 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 047f9a053b..326e56bc85 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-birthday-users instead.' }, }, }, ], @@ -146,14 +146,15 @@ export default class extends Endpoint { // eslint- .andWhere('following.followerId = :userId', { userId: user.id }) .innerJoinAndSelect('following.followee', 'followee'); + // @deprecated use get-following-birthday-users 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() })`); + 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 (err) { throw new ApiError(meta.errors.birthdayInvalid); } diff --git a/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts b/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts new file mode 100644 index 0000000000..124114244e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.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: 'Find 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 { // 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>(( + 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'> }); + }); + } +} -- cgit v1.2.3-freya