Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
commit
9723d01277
|
@ -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`のみに
|
||||||
|
|
|
@ -4865,7 +4865,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"wellKnownWebsites": string;
|
"wellKnownWebsites": string;
|
||||||
/**
|
/**
|
||||||
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、外部サイトへのリダイレクトの警告を省略させることができます。
|
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。ドメイン名だけ書くと後方一致になります。一致した場合、外部サイトへのリダイレクトの警告を省略させることができます。
|
||||||
*/
|
*/
|
||||||
"wellKnownWebsitesDescription": string;
|
"wellKnownWebsitesDescription": string;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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}'`);
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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' });
|
||||||
|
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue