From 01aa56c60201aed3e278193a0aee7c13b1f0aef7 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 31 Dec 2025 14:50:01 +0900
Subject: enhance(backend/oauth): Support client information discovery in the
IndieAuth 11 July 2024 spec (#17030)
* enhance(backend): Support client information discovery in the IndieAuth 11 July 2024 spec
* add tests
* Update Changelog
* Update Changelog
* fix tests
* fix test describe to align with the other describe format
---
.../src/server/oauth/OAuth2ProviderService.ts | 89 ++++++++++++++++------
1 file changed, 67 insertions(+), 22 deletions(-)
(limited to 'packages/backend/src/server/oauth')
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index d2391c43ab..47f4bf947d 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -123,41 +123,84 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str
return { name, logo };
}
-// https://indieauth.spec.indieweb.org/#client-information-discovery
-// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
-// and if there is an [h-app] with a url property matching the client_id URL,
-// then it should use the name and icon and display them on the authorization prompt."
-// (But we don't display any icon for now)
-// https://indieauth.spec.indieweb.org/#redirect-url
-// "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute
-// of redirect_uri at the client_id URL.
-// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
-// look for an exact match of the given redirect_uri in the request against the list of
-// redirect_uris discovered after resolving any relative URLs."
async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise {
try {
const res = await httpRequestService.send(id);
+
const redirectUris: string[] = [];
+ let name = id;
+ let logo: string | null = null;
+ // https://indieauth.spec.indieweb.org/#redirect-url
+ // "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute
+ // of redirect_uri at the client_id URL.
+ // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
+ // look for an exact match of the given redirect_uri in the request against the list of
+ // redirect_uris discovered after resolving any relative URLs."
const linkHeader = res.headers.get('link');
if (linkHeader) {
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
}
- const text = await res.text();
- const doc = htmlParser.parse(`${text}
`);
+ if (res.headers.get('content-type')?.includes('application/json')) {
+ // Client discovery via JSON document (11 July 2024 spec)
+ // https://indieauth.spec.indieweb.org/#client-metadata
+ // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing
+ // client metadata defined in [RFC7591], the minimum properties for an IndieAuth
+ // client defined below."
+
+ const json = await res.json() as {
+ client_id: string;
+ client_name?: string;
+ client_uri: string;
+ logo_uri?: string;
+ redirect_uris?: string[];
+ };
+
+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
+ // "The authorization server MUST verify that the client_id in the document matches the
+ // client_id of the URL where the document was retrieved."
+ if (json.client_id !== id) {
+ throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request');
+ }
- redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
+ // "The client_uri MUST be a prefix of the client_id."
+ if (!json.client_uri || !id.startsWith(json.client_uri)) {
+ throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request');
+ }
- let name = id;
- let logo: string | null = null;
- if (text) {
- const microformats = parseMicroformats(doc, res.url, id);
- if (typeof microformats.name === 'string') {
- name = microformats.name;
+ if (typeof json.client_name === 'string') {
+ name = json.client_name;
}
- if (typeof microformats.logo === 'string') {
- logo = microformats.logo;
+
+ if (typeof json.logo_uri === 'string') {
+ // Since uri can be relative, resolve it against the document URL
+ logo = new URL(json.logo_uri, res.url).toString();
+ }
+
+ if (Array.isArray(json.redirect_uris)) {
+ redirectUris.push(...json.redirect_uris.filter((uri): uri is string => typeof uri === 'string'));
+ }
+ } else {
+ // Client discovery via HTML microformats (12 February 2022 spec)
+ // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery
+ // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
+ // and if there is an [h-app] with a url property matching the client_id URL,
+ // then it should use the name and icon and display them on the authorization prompt."
+ const text = await res.text();
+ const doc = htmlParser.parse(`${text}
`);
+
+ redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
+
+ if (text) {
+ const microformats = parseMicroformats(doc, res.url, id);
+ if (typeof microformats.name === 'string') {
+ name = microformats.name;
+ }
+ if (typeof microformats.logo === 'string') {
+ logo = microformats.logo;
+ }
}
}
@@ -172,6 +215,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
logger.error('Error while fetching client information', { err });
if (err instanceof StatusError) {
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
+ } else if (err instanceof AuthorizationError) {
+ throw err;
} else {
throw new AuthorizationError('Failed to parse client information', 'server_error');
}
--
cgit v1.2.3-freya
From 3980b2ca55cdf6a694ef8cad911e161672c0444f Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 5 Mar 2026 19:24:30 +0900
Subject: fix: review fixes (#17208)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: OAuthのContent-Typeを正しく判定するように
* fix(frontend): fix outdated comments
* fix: storagePersistenceのtop-level awaitを解消
* fix
* fix(frontend): add comment
Co-Authored-By: anatawa12
* fix
* fix: rename `users/get-following-users-by-birthday`
* fix: fix types
* Update MkForm.vue
* refactor utility/storage.ts
---------
Co-authored-by: anatawa12
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 2 +-
packages/backend/src/server/api/endpoint-list.ts | 2 +-
.../src/server/api/endpoints/admin/update-meta.ts | 2 +-
.../src/server/api/endpoints/users/following.ts | 4 +-
.../users/get-following-birthday-users.ts | 167 ---------------------
.../users/get-following-users-by-birthday.ts | 167 +++++++++++++++++++++
.../src/server/oauth/OAuth2ProviderService.ts | 4 +-
packages/frontend/src/components/MkDraggable.vue | 1 +
packages/frontend/src/components/MkForm.vue | 2 +-
.../src/pages/admin/RolesEditorFormula.vue | 2 +-
.../src/pages/page-editor/page-editor.blocks.vue | 2 +-
packages/frontend/src/pages/settings/index.vue | 4 +-
packages/frontend/src/pages/settings/other.vue | 4 +-
packages/frontend/src/utility/storage.ts | 12 +-
.../src/widgets/WidgetBirthdayFollowings.user.vue | 2 +-
.../src/widgets/WidgetBirthdayFollowings.vue | 2 +-
packages/misskey-js/etc/misskey-js.api.md | 8 +-
packages/misskey-js/src/autogen/apiClientJSDoc.ts | 4 +-
packages/misskey-js/src/autogen/endpoint.ts | 6 +-
packages/misskey-js/src/autogen/entities.ts | 4 +-
packages/misskey-js/src/autogen/types.ts | 12 +-
21 files changed, 214 insertions(+), 199 deletions(-)
delete mode 100644 packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts
create mode 100644 packages/backend/src/server/api/endpoints/users/get-following-users-by-birthday.ts
(limited to 'packages/backend/src/server/oauth')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24271d7fa0..520b202f59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
## 2026.2.0
### Note
-- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-birthday-users` をご利用ください。
+- `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-users-by-birthday` をご利用ください。
### General
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
index 9311c80eaa..6679005c3c 100644
--- a/packages/backend/src/server/api/endpoint-list.ts
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -391,7 +391,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
-export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js';
+export * as 'users/get-following-users-by-birthday' from './endpoints/users/get-following-users-by-birthday.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
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 7a8dfc4555..372fe3a25f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -341,7 +341,7 @@ export default class extends Endpoint { // eslint-
if (ps.clientOptions !== undefined) {
set.clientOptions = {
- ...serverSettings.clientOptions,
+ ...this.serverSettings.clientOptions,
...ps.clientOptions,
};
}
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 3b3352858f..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, description: '@deprecated use get-following-birthday-users instead.' },
+ birthday: { ...birthdaySchema, nullable: true, description: '@deprecated use get-following-users-by-birthday instead.' },
},
},
],
@@ -146,7 +146,7 @@ export default class extends Endpoint { // eslint-
.andWhere('following.followerId = :userId', { userId: user.id })
.innerJoinAndSelect('following.followee', 'followee');
- // @deprecated use get-following-birthday-users instead.
+ // @deprecated use get-following-users-by-birthday instead.
if (ps.birthday) {
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
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
deleted file mode 100644
index 124114244e..0000000000
--- a/packages/backend/src/server/api/endpoints/users/get-following-birthday-users.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * 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'> });
- });
- }
-}
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 { // 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'> });
+ });
+ }
+}
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index 47f4bf947d..840c34b806 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -142,7 +142,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
}
- if (res.headers.get('content-type')?.includes('application/json')) {
+ const contentType = res.headers.get('content-type');
+ const mediaType = contentType ? contentType.split(';')[0].trim() : null;
+ if (mediaType === 'application/json') {
// Client discovery via JSON document (11 July 2024 spec)
// https://indieauth.spec.indieweb.org/#client-metadata
// "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing
diff --git a/packages/frontend/src/components/MkDraggable.vue b/packages/frontend/src/components/MkDraggable.vue
index 07fa1045b9..6e2e038f87 100644
--- a/packages/frontend/src/components/MkDraggable.vue
+++ b/packages/frontend/src/components/MkDraggable.vue
@@ -109,6 +109,7 @@ function onDragstart(ev: DragEvent, item: T) {
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
+ // SEE: https://issues.chromium.org/issues/41150279
window.setTimeout(() => {
dragging.value = true;
}, 10);
diff --git a/packages/frontend/src/components/MkForm.vue b/packages/frontend/src/components/MkForm.vue
index 1ece0ad4c3..f2360e8cdd 100644
--- a/packages/frontend/src/components/MkForm.vue
+++ b/packages/frontend/src/components/MkForm.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
+
onSavingStateChange(k, changed, invalid)">
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 13f66662d0..384282262d 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
-
+
-
+
removeItem(item)"/>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index f9529fa352..abfac37275 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -51,10 +51,12 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
-import { enableStoragePersistence, storagePersisted, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const indexInfo = {
title: i18n.ts.settings,
icon: 'ti ti-settings',
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 660647fff7..4facc696a4 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -165,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
-import { enableStoragePersistence, storagePersisted, storagePersistenceSupported } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -180,6 +180,8 @@ import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin();
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const reportError = prefer.model('reportError');
const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');
diff --git a/packages/frontend/src/utility/storage.ts b/packages/frontend/src/utility/storage.ts
index 86f4b8b3c3..42743f78ea 100644
--- a/packages/frontend/src/utility/storage.ts
+++ b/packages/frontend/src/utility/storage.ts
@@ -3,13 +3,21 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ref } from 'vue';
+import { readonly, ref } from 'vue';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
export const storagePersistenceSupported = window.isSecureContext && 'storage' in navigator;
-export const storagePersisted = ref(storagePersistenceSupported ? await navigator.storage.persisted() : false);
+const storagePersisted = ref(false);
+
+export async function getStoragePersistenceStatusRef() {
+ if (storagePersistenceSupported) {
+ storagePersisted.value = await navigator.storage.persisted().catch(() => false);
+ }
+
+ return readonly(storagePersisted);
+}
export async function enableStoragePersistence() {
if (!storagePersistenceSupported) return;
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue
index dc8ffcc818..2b714c2f6c 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.user.vue
@@ -30,7 +30,7 @@ import { useLowresTime } from '@/composables/use-lowres-time.js';
import { userPage, acct } from '@/filters/user.js';
const props = defineProps<{
- item: Misskey.entities.UsersGetFollowingBirthdayUsersResponse[number];
+ item: Misskey.entities.UsersGetFollowingUsersByBirthdayResponse[number];
}>();
const now = useLowresTime();
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index ea577f3219..cf9c5a3d35 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -106,7 +106,7 @@ const end = computed(() => {
}
});
-const birthdayUsersPaginator = markRaw(new Paginator('users/get-following-birthday-users', {
+const birthdayUsersPaginator = markRaw(new Paginator('users/get-following-users-by-birthday', {
limit: 18,
offsetMode: true,
computedParams: computed(() => {
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index b84fdc7e31..760d5c319b 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2128,8 +2128,8 @@ declare namespace entities {
UsersFollowingResponse,
UsersGalleryPostsRequest,
UsersGalleryPostsResponse,
- UsersGetFollowingBirthdayUsersRequest,
- UsersGetFollowingBirthdayUsersResponse,
+ UsersGetFollowingUsersByBirthdayRequest,
+ UsersGetFollowingUsersByBirthdayResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
UsersListsCreateRequest,
@@ -3741,10 +3741,10 @@ type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBo
type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
// @public (undocumented)
-type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
+type UsersGetFollowingUsersByBirthdayRequest = operations['users___get-following-users-by-birthday']['requestBody']['content']['application/json'];
// @public (undocumented)
-type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
+type UsersGetFollowingUsersByBirthdayResponse = operations['users___get-following-users-by-birthday']['responses']['200']['content']['application/json'];
// @public (undocumented)
type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 91f7bb5b07..c9cb9147f1 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -4533,11 +4533,11 @@ declare module '../api.js' {
): Promise>;
/**
- * Find users who have a birthday on the specified range.
+ * Retrieve users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- request(
+ request(
endpoint: E,
params: P,
credential?: string | null,
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 8dc3b26bd7..2d4e0fe35e 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -616,8 +616,8 @@ import type {
UsersFollowingResponse,
UsersGalleryPostsRequest,
UsersGalleryPostsResponse,
- UsersGetFollowingBirthdayUsersRequest,
- UsersGetFollowingBirthdayUsersResponse,
+ UsersGetFollowingUsersByBirthdayRequest,
+ UsersGetFollowingUsersByBirthdayResponse,
UsersGetFrequentlyRepliedUsersRequest,
UsersGetFrequentlyRepliedUsersResponse,
UsersListsCreateRequest,
@@ -1069,7 +1069,7 @@ export type Endpoints = {
'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse };
'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse };
'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse };
- 'users/get-following-birthday-users': { req: UsersGetFollowingBirthdayUsersRequest; res: UsersGetFollowingBirthdayUsersResponse };
+ 'users/get-following-users-by-birthday': { req: UsersGetFollowingUsersByBirthdayRequest; res: UsersGetFollowingUsersByBirthdayResponse };
'users/get-frequently-replied-users': { req: UsersGetFrequentlyRepliedUsersRequest; res: UsersGetFrequentlyRepliedUsersResponse };
'users/lists/create': { req: UsersListsCreateRequest; res: UsersListsCreateResponse };
'users/lists/create-from-public': { req: UsersListsCreateFromPublicRequest; res: UsersListsCreateFromPublicResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index e9bb080636..a49dd729e6 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -619,8 +619,8 @@ export type UsersFollowingRequest = operations['users___following']['requestBody
export type UsersFollowingResponse = operations['users___following']['responses']['200']['content']['application/json'];
export type UsersGalleryPostsRequest = operations['users___gallery___posts']['requestBody']['content']['application/json'];
export type UsersGalleryPostsResponse = operations['users___gallery___posts']['responses']['200']['content']['application/json'];
-export type UsersGetFollowingBirthdayUsersRequest = operations['users___get-following-birthday-users']['requestBody']['content']['application/json'];
-export type UsersGetFollowingBirthdayUsersResponse = operations['users___get-following-birthday-users']['responses']['200']['content']['application/json'];
+export type UsersGetFollowingUsersByBirthdayRequest = operations['users___get-following-users-by-birthday']['requestBody']['content']['application/json'];
+export type UsersGetFollowingUsersByBirthdayResponse = operations['users___get-following-users-by-birthday']['responses']['200']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersRequest = operations['users___get-frequently-replied-users']['requestBody']['content']['application/json'];
export type UsersGetFrequentlyRepliedUsersResponse = operations['users___get-frequently-replied-users']['responses']['200']['content']['application/json'];
export type UsersListsCreateRequest = operations['users___lists___create']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 5b6da957f7..46d04ac2dc 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3717,14 +3717,14 @@ export type paths = {
*/
post: operations['users___gallery___posts'];
};
- '/users/get-following-birthday-users': {
+ '/users/get-following-users-by-birthday': {
/**
- * users/get-following-birthday-users
- * @description Find users who have a birthday on the specified range.
+ * users/get-following-users-by-birthday
+ * @description Retrieve users who have a birthday on the specified range.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
- post: operations['users___get-following-birthday-users'];
+ post: operations['users___get-following-users-by-birthday'];
};
'/users/get-frequently-replied-users': {
/**
@@ -34882,7 +34882,7 @@ export interface operations {
untilDate?: number;
/** @default 10 */
limit?: number;
- /** @description @deprecated use get-following-birthday-users instead. */
+ /** @description @deprecated use get-following-users-by-birthday instead. */
birthday?: string | null;
};
};
@@ -35018,7 +35018,7 @@ export interface operations {
};
};
};
- 'users___get-following-birthday-users': {
+ 'users___get-following-users-by-birthday': {
requestBody: {
content: {
'application/json': {
--
cgit v1.2.3-freya