Merge remote-tracking branch 'misskey-dev/develop' into io

This commit is contained in:
まっちゃとーにゅ 2024-03-28 19:45:50 +09:00
commit 9723d01277
No known key found for this signature in database
GPG Key ID: 6AFBBF529601C1DB
10 changed files with 1873 additions and 1837 deletions

View File

@ -18,6 +18,7 @@
- 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
- Enhance: ページのデザインを変更 - Enhance: ページのデザインを変更
- Enhance: 2要素認証ワンタイムパスワードの入力欄を改善 - Enhance: 2要素認証ワンタイムパスワードの入力欄を改善
- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正 - Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される - Fix: ローカルURLのプレビューポップアップが左上に表示される
@ -27,6 +28,7 @@
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177 - Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
- CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。 - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正
### Server ### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに

2
locales/index.d.ts vendored
View File

@ -4865,7 +4865,7 @@ export interface Locale extends ILocale {
*/ */
"wellKnownWebsites": string; "wellKnownWebsites": string;
/** /**
* AND指定になりOR指定になります * AND指定になりOR指定になります
*/ */
"wellKnownWebsitesDescription": string; "wellKnownWebsitesDescription": string;
/** /**

View File

@ -6,6 +6,7 @@
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
import { birthdaySchema } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
@ -66,7 +67,7 @@ export const paramDef = {
description: 'The local host is represented with `null`.', description: 'The local host is represented with `null`.',
}, },
birthday: { type: 'string', nullable: true }, birthday: { ...birthdaySchema, nullable: true },
}, },
anyOf: [ anyOf: [
{ required: ['userId'] }, { required: ['userId'] },
@ -127,9 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.birthday) { if (ps.birthday) {
try { try {
const d = new Date(ps.birthday); const birthday = ps.birthday.substring(5, 10);
d.setHours(0, 0, 0, 0);
const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
birthdayUserQuery.select('user_profile.userId') birthdayUserQuery.select('user_profile.userId')
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);

View File

@ -93,7 +93,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1); const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = { const info = {
operationId: endpoint.name, operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
summary: endpoint.name, summary: endpoint.name,
description: desc, description: desc,
externalDocs: { externalDocs: {

View File

@ -160,19 +160,17 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
/*
test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => { test('フォローしているユーザーの visibility: followers な投稿への返信が流れる', async () => {
const note = await api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko); const note = await post(kyoko, { text: 'foo', visibility: 'followers' });
const fired = await waitFire( const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.body.id }, kyoko), // kyoko posts () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo', msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo',
); );
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
*/
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => { test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' });

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings"> <MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
<template #icon><i class="ti ti-cake"></i></template> <template #icon><i class="ti ti-cake"></i></template>
<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template> <template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template>
<div :class="$style.bdayFRoot"> <div :class="$style.bdayFRoot">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
@ -53,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit, emit,
); );
const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]); const users = ref<Misskey.Endpoints['users/following']['res']>([]);
const fetching = ref(true); const fetching = ref(true);
let lastFetchedAt = '1970-01-01'; let lastFetchedAt = '1970-01-01';
@ -70,19 +71,35 @@ const fetch = () => {
now.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0);
if (now > lfAtD) { if (now > lfAtD) {
misskeyApi('users/following', { actualFetch();
limit: 18,
birthday: now.toISOString(),
userId: $i.id,
}).then(res => {
users.value = res;
fetching.value = false;
});
lastFetchedAt = now.toISOString(); lastFetchedAt = now.toISOString();
} }
}; };
function actualFetch() {
if ($i == null) {
users.value = [];
fetching.value = false;
return;
}
const now = new Date();
now.setHours(0, 0, 0, 0);
fetching.value = true;
misskeyApi('users/following', {
limit: 18,
birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`,
userId: $i.id,
}).then(res => {
users.value = res;
window.setTimeout(() => {
//
fetching.value = false;
}, 100);
});
}
useInterval(fetch, 1000 * 60, { useInterval(fetch, 1000 * 60, {
immediate: true, immediate: true,
afterMounted: true, afterMounted: true,

File diff suppressed because it is too large Load Diff

View File

@ -60,13 +60,17 @@ async function generateEndpoints(
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
const paths = openApiDocs.paths ?? {}; const paths = openApiDocs.paths ?? {};
const postPathItems = Object.keys(paths) const postPathItems = Object.keys(paths)
.map(it => paths[it]?.post) .map(it => ({
_path_: it.replace(/^\//, ''),
...paths[it]?.post,
}))
.filter(filterUndefined); .filter(filterUndefined);
for (const operation of postPathItems) { for (const operation of postPathItems) {
const path = operation._path_;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const operationId = operation.operationId!; const operationId = operation.operationId!;
const endpoint = new Endpoint(operationId); const endpoint = new Endpoint(path);
endpoints.push(endpoint); endpoints.push(endpoint);
if (isRequestBodyObject(operation.requestBody)) { if (isRequestBodyObject(operation.requestBody)) {
@ -76,19 +80,21 @@ async function generateEndpoints(
// いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする // いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする
endpoint.request = new OperationTypeAlias( endpoint.request = new OperationTypeAlias(
operationId, operationId,
path,
supportMediaTypes[0], supportMediaTypes[0],
OperationsAliasType.REQUEST, OperationsAliasType.REQUEST,
); );
} }
} }
if (isResponseObject(operation.responses['200']) && operation.responses['200'].content) { if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
const resContent = operation.responses['200'].content; const resContent = operation.responses['200'].content;
const supportMediaTypes = Object.keys(resContent); const supportMediaTypes = Object.keys(resContent);
if (supportMediaTypes.length > 0) { if (supportMediaTypes.length > 0) {
// いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする // いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする
endpoint.response = new OperationTypeAlias( endpoint.response = new OperationTypeAlias(
operationId, operationId,
path,
supportMediaTypes[0], supportMediaTypes[0],
OperationsAliasType.RESPONSE, OperationsAliasType.RESPONSE,
); );
@ -98,6 +104,8 @@ async function generateEndpoints(
const entitiesOutputLine: string[] = []; const entitiesOutputLine: string[] = [];
entitiesOutputLine.push('/* eslint @typescript-eslint/naming-convention: 0 */');
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`); entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
entitiesOutputLine.push(''); entitiesOutputLine.push('');
@ -138,12 +146,19 @@ async function generateApiClientJSDoc(
endpointsFileName: string, endpointsFileName: string,
warningsOutputPath: string, warningsOutputPath: string,
) { ) {
const endpoints: { operationId: string; description: string; }[] = []; const endpoints: {
operationId: string;
path: string;
description: string;
}[] = [];
// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり // misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
const paths = openApiDocs.paths ?? {}; const paths = openApiDocs.paths ?? {};
const postPathItems = Object.keys(paths) const postPathItems = Object.keys(paths)
.map(it => paths[it]?.post) .map(it => ({
_path_: it.replace(/^\//, ''),
...paths[it]?.post,
}))
.filter(filterUndefined); .filter(filterUndefined);
for (const operation of postPathItems) { for (const operation of postPathItems) {
@ -153,6 +168,7 @@ async function generateApiClientJSDoc(
if (operation.description) { if (operation.description) {
endpoints.push({ endpoints.push({
operationId: operationId, operationId: operationId,
path: operation._path_,
description: operation.description, description: operation.description,
}); });
} }
@ -173,7 +189,7 @@ async function generateApiClientJSDoc(
' /**', ' /**',
` * ${endpoint.description.split('\n').join('\n * ')}`, ` * ${endpoint.description.split('\n').join('\n * ')}`,
' */', ' */',
` request<E extends '${endpoint.operationId}', P extends Endpoints[E][\'req\']>(`, ` request<E extends '${endpoint.path}', P extends Endpoints[E][\'req\']>(`,
' endpoint: E,', ' endpoint: E,',
' params: P,', ' params: P,',
' credential?: string | null,', ' credential?: string | null,',
@ -232,21 +248,24 @@ interface IOperationTypeAlias {
class OperationTypeAlias implements IOperationTypeAlias { class OperationTypeAlias implements IOperationTypeAlias {
public readonly operationId: string; public readonly operationId: string;
public readonly path: string;
public readonly mediaType: string; public readonly mediaType: string;
public readonly type: OperationsAliasType; public readonly type: OperationsAliasType;
constructor( constructor(
operationId: string, operationId: string,
path: string,
mediaType: string, mediaType: string,
type: OperationsAliasType, type: OperationsAliasType,
) { ) {
this.operationId = operationId; this.operationId = operationId;
this.path = path;
this.mediaType = mediaType; this.mediaType = mediaType;
this.type = type; this.type = type;
} }
generateName(): string { generateName(): string {
const nameBase = this.operationId.replace(/\//g, '-'); const nameBase = this.path.replace(/\//g, '-');
return toPascal(nameBase + this.type); return toPascal(nameBase + this.type);
} }
@ -279,19 +298,19 @@ const emptyRequest = new EmptyTypeAlias(OperationsAliasType.REQUEST);
const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE); const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE);
class Endpoint { class Endpoint {
public readonly operationId: string; public readonly path: string;
public request?: IOperationTypeAlias; public request?: IOperationTypeAlias;
public response?: IOperationTypeAlias; public response?: IOperationTypeAlias;
constructor(operationId: string) { constructor(path: string) {
this.operationId = operationId; this.path = path;
} }
toLine(): string { toLine(): string {
const reqName = this.request?.generateName() ?? emptyRequest.generateName(); const reqName = this.request?.generateName() ?? emptyRequest.generateName();
const resName = this.response?.generateName() ?? emptyResponse.generateName(); const resName = this.response?.generateName() ?? emptyResponse.generateName();
return `'${this.operationId}': { req: ${reqName}; res: ${resName} };`; return `'${this.path}': { req: ${reqName}; res: ${resName} };`;
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff