Merge branch 'develop' into feature/emoji-grid

This commit is contained in:
おさむのひと 2024-07-07 11:58:32 +09:00 committed by GitHub
commit 1cefedb6ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
218 changed files with 6790 additions and 5326 deletions

View File

@ -1,5 +1,3 @@
version: '3.8'
services: services:
app: app:
build: build:

View File

@ -1,6 +1,6 @@
{ {
"name": "Misskey", "name": "Misskey",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "compose.yml",
"service": "app", "service": "app",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"features": { "features": {

View File

@ -7,7 +7,7 @@ Dockerfile
build/ build/
built/ built/
db/ db/
docker-compose.yml .devcontainer/compose.yml
node_modules/ node_modules/
packages/*/node_modules packages/*/node_modules
redis/ redis/
@ -28,4 +28,4 @@ fluent-emojis/
.idea/ .idea/
packages/*/.vscode/ packages/*/.vscode/
packages/backend/test/docker-compose.yml packages/backend/test/compose.yml

View File

@ -2,3 +2,7 @@ contact_links:
- name: 💬 Misskey official Discord - name: 💬 Misskey official Discord
url: https://discord.gg/Wp8gVStHW3 url: https://discord.gg/Wp8gVStHW3
about: Chat freely about Misskey about: Chat freely about Misskey
# 仮
- name: 💬 Start discussion
url: https://github.com/misskey-dev/misskey/discussions
about: The official forum to join conversation and ask question

View File

@ -37,7 +37,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -48,7 +48,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub - name: Build and Push to Docker Hub
id: build id: build
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -13,14 +13,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKLE_VERSION: 0.4.14
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: | - name: Download and install dockle v${{ env.DOCKLE_VERSION }}
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v${DOCKLE_VERSION}/dockle_${DOCKLE_VERSION}_Linux-64bit.deb"
sudo dpkg -i dockle.deb sudo dpkg -i dockle.deb
- run: | - run: |
cp .config/docker_example.env .config/docker.env cp .config/docker_example.env .config/docker.env
cp ./docker-compose_example.yml ./docker-compose.yml cp ./compose_example.yml ./compose.yml
- run: | - run: |
docker compose up -d web docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest

View File

@ -10,14 +10,14 @@ on:
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
jobs: jobs:
pnpm_install: pnpm_install:

View File

@ -88,7 +88,7 @@ jobs:
if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then if [ "$BRANCH" = "misskey-dev:$HEAD_REF" ]; then
BRANCH="$HEAD_REF" BRANCH="$HEAD_REF"
fi fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name "$BRANCH" $(echo "$CHROMATIC_PARAMETER")
env: env:
HEAD_REF: ${{ github.event.pull_request.head.ref }} HEAD_REF: ${{ github.event.pull_request.head.ref }}
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

4
.gitignore vendored
View File

@ -35,8 +35,8 @@ coverage
!/.config/example.yml !/.config/example.yml
!/.config/docker_example.yml !/.config/docker_example.yml
!/.config/docker_example.env !/.config/docker_example.env
docker-compose.yml .devcontainer/compose.yml
!/.devcontainer/docker-compose.yml !/.devcontainer/compose.yml
# misskey # misskey
/build /build

View File

@ -3,15 +3,34 @@
### General ### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Feat: カスタム絵文字管理画面をリニューアル #10996 - Feat: カスタム絵文字管理画面をリニューアル #10996
* β版として公開のため、旧画面も引き続き利用可能です * β版として公開のため、旧画面も引き続き利用可能です
### Client ### Client
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
- Fix: アンテナの編集画面のボタンに隙間を追加
- Fix: テーマプレビューが見れない問題を修正
### Server ### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように
## 2024.5.0 ## 2024.5.0

View File

@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/compose.yaml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View File

@ -82,6 +82,10 @@ RUN apt-get update \
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey
# add package.json to add pnpm
COPY --chown=misskey:misskey ./package.json ./package.json
RUN corepack install
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules

View File

@ -1,5 +1,3 @@
version: "3"
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します # このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services: services:

View File

@ -1,5 +1,3 @@
version: "3"
services: services:
web: web:
build: . build: .

8
locales/index.d.ts vendored
View File

@ -9765,7 +9765,7 @@ export interface Locale extends ILocale {
"_dataSaver": { "_dataSaver": {
"_media": { "_media": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**
@ -9775,7 +9775,7 @@ export interface Locale extends ILocale {
}; };
"_avatar": { "_avatar": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**
@ -9785,7 +9785,7 @@ export interface Locale extends ILocale {
}; };
"_urlPreview": { "_urlPreview": {
/** /**
* URLプレビューのサムネイル * URLプレビューのサムネイルを非表示
*/ */
"title": string; "title": string;
/** /**
@ -9795,7 +9795,7 @@ export interface Locale extends ILocale {
}; };
"_code": { "_code": {
/** /**
* *
*/ */
"title": string; "title": string;
/** /**

View File

@ -52,7 +52,11 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() { export function build() {
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); // vitestの挙動を調整するため、一度ローカル変数化する必要がある
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
const metaUrl = import.meta.url;
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => { const removeEmpty = (obj) => {

View File

@ -2600,16 +2600,16 @@ _externalResourceInstaller:
_dataSaver: _dataSaver:
_media: _media:
title: "メディアの読み込み" title: "メディアの読み込みを無効化"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。" description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar: _avatar:
title: "アイコン画像" title: "アイコン画像のアニメーションを無効化"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview: _urlPreview:
title: "URLプレビューのサムネイル" title: "URLプレビューのサムネイルを非表示"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。" description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code: _code:
title: "コードハイライト" title: "コードハイライトを非表示"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere: _hemisphere:

View File

@ -55,20 +55,22 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.38", "postcss": "8.4.38",
"tar": "6.2.1", "tar": "6.2.1",
"terser": "5.30.3", "terser": "5.31.1",
"typescript": "5.4.5", "typescript": "5.5.3",
"esbuild": "0.20.2", "esbuild": "0.22.0",
"glob": "10.3.12" "glob": "10.3.12"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.12.7", "@misskey-dev/eslint-plugin": "2.0.2",
"@typescript-eslint/eslint-plugin": "7.7.1", "@types/node": "20.14.9",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.7.3", "cypress": "13.13.0",
"eslint": "8.57.0", "eslint": "9.6.0",
"globals": "15.7.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.3" "start-server-and-test": "2.0.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0" "@tensorflow/tfjs-core": "4.4.0"

View File

@ -1,4 +0,0 @@
node_modules
/built
/.eslintrc.js
/@types/**/*

View File

@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', './test/tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@ -0,0 +1,46 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: ['**/node_modules', 'built', '@types/**/*'],
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json', './test/tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^20.10.0" "node": "^20.10.0 || ^22.0.0"
}, },
"scripts": { "scripts": {
"start": "node ./built/boot/entry.js", "start": "node ./built/boot/entry.js",
@ -65,43 +65,43 @@
"utf-8-validate": "6.0.3" "utf-8-validate": "6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.412.0", "@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.412.0", "@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.17.0", "@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.17.0", "@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.17.0", "@bull-board/ui": "5.20.5",
"@discordapp/twemoji": "15.0.3", "@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0", "@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1", "@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1", "@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0", "@fastify/express": "3.0.0",
"@fastify/http-proxy": "9.5.0", "@fastify/http-proxy": "9.5.0",
"@fastify/multipart": "8.2.0", "@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.3", "@fastify/static": "7.0.4",
"@fastify/view": "9.1.0", "@fastify/view": "9.1.0",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.52", "@napi-rs/canvas": "^0.1.53",
"@nestjs/common": "10.3.8", "@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.8", "@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.8", "@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0", "@sentry/node": "8.13.0",
"@sentry/profiling-node": "^8.5.0", "@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0", "@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12", "@swc/cli": "0.3.12",
"@swc/core": "1.4.17", "@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.13.0", "ajv": "8.16.0",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "5.7.8", "bullmq": "5.8.3",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -112,27 +112,27 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.26.2", "fastify": "4.28.1",
"fastify-raw-body": "4.3.0", "fastify-raw-body": "4.3.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.0.0", "file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.2.1", "got": "14.4.1",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"ip-cidr": "3.1.0", "ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.0.0", "is-svg": "5.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "24.0.0", "jsdom": "24.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.2", "jsonld": "8.3.2",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"meilisearch": "0.38.0", "meilisearch": "0.41.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
@ -142,24 +142,24 @@
"nanoid": "5.0.7", "nanoid": "5.0.7",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.13", "nodemailer": "6.9.14",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.2.3", "otpauth": "9.3.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.5", "pg": "8.12.0",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.20.10", "re2": "1.21.3",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
@ -167,27 +167,26 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.33.3", "sharp": "0.33.4",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.22.7", "systeminformation": "5.22.11",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
"typescript": "5.4.5", "typescript": "5.5.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
"ws": "8.17.0", "ws": "8.17.1",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0", "@nestjs/platform-express": "10.3.10",
"@nestjs/platform-express": "10.3.8",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -197,22 +196,21 @@
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3", "@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6", "@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.13", "@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14", "@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.12.7", "@types/node": "20.14.9",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.15", "@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4", "@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.5", "@types/pg": "8.11.6",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode": "2.1.4", "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
@ -228,18 +226,17 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/parser": "7.15.0",
"aws-sdk-client-mock": "3.0.1", "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"execa": "8.0.1", "execa": "9.2.0",
"fkill": "^9.0.0", "fkill": "9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.0", "nodemon": "3.1.4",
"pid-port": "1.0.0", "pid-port": "1.0.0",
"simple-oauth2": "5.0.0" "simple-oauth2": "5.0.1"
} }
} }

View File

@ -30,6 +30,7 @@ function execStart() {
async function killProc() { async function killProc() {
if (backendProcess) { if (backendProcess) {
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill(); backendProcess.kill();
await new Promise(resolve => backendProcess.on('exit', resolve)); await new Promise(resolve => backendProcess.on('exit', resolve));
backendProcess = undefined; backendProcess = undefined;
@ -46,6 +47,7 @@ async function killProc() {
], ],
{ {
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
serialization: "json",
}) })
.on('message', async (message) => { .on('message', async (message) => {
if (message.type === 'exit') { if (message.type === 'exit') {

View File

@ -10,7 +10,6 @@ import sanitizeHtml from 'sanitize-html';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { import type {
AbuseReportNotificationRecipientRepository, AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient, MiAbuseReportNotificationRecipient,
@ -91,7 +90,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it
.filter(it => it.isActive && it.userProfile?.emailVerified) .filter(it => it.isActive && it.userProfile?.emailVerified)
.map(it => it.userProfile?.email) .map(it => it.userProfile?.email)
.filter(isNotNull), .filter(x => x != null),
); );
// 送信先の鮮度を保つため、毎回取得する // 送信先の鮮度を保つため、毎回取得する
@ -138,7 +137,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.then(it => it .then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
.map(it => it.systemWebhookId) .map(it => it.systemWebhookId)
.filter(isNotNull)); .filter(x => x != null));
for (const webhookId of recipientWebhookIds) { for (const webhookId of recipientWebhookIds) {
await Promise.all( await Promise.all(
abuseReports.map(it => { abuseReports.map(it => {
@ -340,7 +339,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> { private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> {
const userRecipients = recipients.filter(it => it.userId !== null); const userRecipients = recipients.filter(it => it.userId !== null);
const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(isNotNull)); const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null));
if (recipientUserIds.size <= 0) { if (recipientUserIds.size <= 0) {
// ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い // ユーザが通知先として設定されていない場合、この関数での処理を行うべきレコードが無い
return recipients; return recipients;

View File

@ -41,7 +41,7 @@ export class ClipService {
const currentCount = await this.clipsRepository.countBy({ const currentCount = await this.clipsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ClipService.TooManyClipsError(); throw new ClipService.TooManyClipsError();
} }
@ -102,7 +102,7 @@ export class ClipService {
const currentCount = await this.clipNotesRepository.countBy({ const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id, clipId: clip.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError(); throw new ClipService.TooManyClipNotesError();
} }

View File

@ -55,9 +55,6 @@ export class FanoutTimelineEndpointService {
@bindThis @bindThis
private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> { private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
let noteIds: string[];
let shouldFallbackToDb = false;
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
@ -67,12 +64,11 @@ export class FanoutTimelineEndpointService {
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい // TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
const redisResultIds = Array.from(new Set(redisResult.flat(1))); const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare);
redisResultIds.sort(idCompare); let noteIds = redisResultIds.slice(0, ps.limit);
noteIds = redisResultIds.slice(0, ps.limit); const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
if (!shouldFallbackToDb) { if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true); let filter = ps.noteFilter ?? (_note => true);

View File

@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js'; import type * as mfm from 'mfm-js';
const treeAdapter = TreeAdapter.defaultTreeAdapter; const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@ -46,7 +48,7 @@ export class MfmService {
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
@ -58,7 +60,7 @@ export class MfmService {
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
analyze(n); analyze(n);
@ -66,14 +68,16 @@ export class MfmService {
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) {
return;
}
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
@ -81,8 +85,7 @@ export class MfmService {
break; break;
} }
case 'a': case 'a': {
{
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
@ -130,8 +133,7 @@ export class MfmService {
break; break;
} }
case 'h1': case 'h1': {
{
text += '【'; text += '【';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '】\n'; text += '】\n';
@ -139,16 +141,14 @@ export class MfmService {
} }
case 'b': case 'b':
case 'strong': case 'strong': {
{
text += '**'; text += '**';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small': {
{
text += '<small>'; text += '<small>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</small>'; text += '</small>';
@ -156,8 +156,7 @@ export class MfmService {
} }
case 's': case 's':
case 'del': case 'del': {
{
text += '~~'; text += '~~';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '~~'; text += '~~';
@ -165,8 +164,7 @@ export class MfmService {
} }
case 'i': case 'i':
case 'em': case 'em': {
{
text += '<i>'; text += '<i>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</i>'; text += '</i>';
@ -207,8 +205,7 @@ export class MfmService {
case 'h3': case 'h3':
case 'h4': case 'h4':
case 'h5': case 'h5':
case 'h6': case 'h6': {
{
text += '\n\n'; text += '\n\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
@ -221,8 +218,7 @@ export class MfmService {
case 'article': case 'article':
case 'li': case 'li':
case 'dt': case 'dt':
case 'dd': case 'dd': {
{
text += '\n'; text += '\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;

View File

@ -59,7 +59,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -839,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens); const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m => let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(isNotNull); ))).filter(x => x != null);
// Drop duplicate users // Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) => mentionedUsers = mentionedUsers.filter((u, i, self) =>

View File

@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764'; const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@ -117,11 +118,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
} }
// Check if note is Renote
if (isRenote(note) && !isQuote(note)) {
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
}
let reaction = _reaction ?? FALLBACK; let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764'; reaction = '\u2764';
} else if (_reaction) { } else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp); const custom = reaction.match(isCustomEmojiRegexp);
if (custom) { if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host); const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({ const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id, userListId: list.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError(); throw new UserListService.TooManyUsersError();
} }

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js'; import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js'; import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js'; import type { ApObject } from './type.js';
@ -41,7 +40,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter(isNotNull); )).filter(x => x != null);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {

View File

@ -27,7 +27,6 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
@ -538,7 +537,7 @@ export class ApInboxService {
const userIds = uris const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/')) .filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1)) .map(uri => uri.split('/').at(-1))
.filter(isNotNull); .filter(x => x != null);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });

View File

@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { JsonLdService } from './JsonLdService.js'; import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
@ -317,7 +316,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => { const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter(isNotNull); return ids.map(id => items.find(item => item.id === id)).filter(x => x != null);
}; };
let inReplyTo; let inReplyTo;
@ -686,7 +685,7 @@ export class ApRendererService {
if (names.length === 0) return []; if (names.length === 0) return [];
const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null);
return emojis; return emojis;
} }

View File

@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js'; import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js'; import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js'; import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js'; import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js'; import { ApPersonService } from './ApPersonService.js';
@ -28,7 +27,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter(isNotNull); )).filter(x => x != null);
return mentionedUsers; return mentionedUsers;
} }

View File

@ -24,7 +24,6 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
@ -253,7 +252,7 @@ export class ApNoteService {
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull)); const uris = unique([note._misskey_quote, note.quoteUrl].filter(x => x != null));
const results = await Promise.all(uris.map(tryResolveNote)); const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);

View File

@ -38,7 +38,6 @@ import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -637,7 +636,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter(isNotNull)) { for (const note of featuredNotes.filter(x => x != null)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, { transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td), id: this.idService.gen(Date.now() + td),

View File

@ -10,7 +10,6 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js'; import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
@ -52,7 +51,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf'] const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name) ?.map((x) => x.name)
.filter(isNotNull) .filter(x => x != null)
?? []; ?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);

View File

@ -4,7 +4,6 @@
*/ */
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => { return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/); const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null; return m ? m[1] : null;
}).filter(isNotNull); }).filter(x => x != null);
} }
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {

View File

@ -11,7 +11,6 @@ import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable() @Injectable()
export class AbuseReportNotificationRecipientEntityService { export class AbuseReportNotificationRecipientEntityService {
@ -66,13 +65,13 @@ export class AbuseReportNotificationRecipientEntityService {
); );
} }
const userIds = objs.map(it => it.userId).filter(isNotNull); const userIds = objs.map(it => it.userId).filter(x => x != null);
const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0) const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0)
? await this.userEntityService.packMany(userIds) ? await this.userEntityService.packMany(userIds)
.then(it => new Map(it.map(it => [it.id, it]))) .then(it => new Map(it.map(it => [it.id, it])))
: new Map(); : new Map();
const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(isNotNull); const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null);
const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0) const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0)
? await this.systemWebhookEntityService.packMany(systemWebhookIds) ? await this.systemWebhookEntityService.packMany(systemWebhookIds)
.then(it => new Map(it.map(it => [it.id, it]))) .then(it => new Map(it.map(it => [it.id, it])))

View File

@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@ -63,7 +62,7 @@ export class AbuseUserReportEntityService {
) { ) {
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull); const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany( const _userMap = await this.userEntityService.packMany(
[..._reporters, ..._targetUsers, ..._assignees], [..._reporters, ..._targetUsers, ..._assignees],
null, null,

View File

@ -53,7 +53,7 @@ export class ClipEntityService {
isPublic: clip.isPublic, isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined,
}); });
} }

View File

@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js';
import { deepClone } from '@/misc/clone.js'; import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { UtilityService } from '../UtilityService.js'; import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js'; import { VideoProcessingService } from '../VideoProcessingService.js';
@ -261,11 +260,11 @@ export class DriveFileEntityService {
files: MiDriveFile[], files: MiDriveFile[],
options?: PackOptions, options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull); const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(_user) const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user]))); .then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {}))); const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull); return items.filter(x => x != null);
} }
@bindThis @bindThis
@ -290,6 +289,6 @@ export class DriveFileEntityService {
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
if (fileIds.length === 0) return []; if (fileIds.length === 0) return [];
const filesMap = await this.packManyByIdsMap(fileIds, options); const filesMap = await this.packManyByIdsMap(fileIds, options);
return fileIds.map(id => filesMap.get(id)).filter(isNotNull); return fileIds.map(id => filesMap.get(id)).filter(x => x != null);
} }
} }

View File

@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -59,8 +58,8 @@ export class InviteCodeEntityService {
tickets: MiRegistrationTicket[], tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull); const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null);
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull); const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null);
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me) const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
.then(users => new Map(users.map(u => [u.id, u]))); .then(users => new Map(users.map(u => [u.id, u])));
return Promise.all( return Promise.all(

View File

@ -14,7 +14,6 @@ import type { MiNote } from '@/models/Note.js';
import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { MiNoteReaction } from '@/models/NoteReaction.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { DebounceLoader } from '@/misc/loader.js'; import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -276,7 +275,7 @@ export class NoteEntityService implements OnModuleInit {
packedFiles.set(k, v); packedFiles.set(k, v);
} }
} }
return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
} }
@bindThis @bindThis
@ -449,12 +448,12 @@ export class NoteEntityService implements OnModuleInit {
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [ const users = [
...notes.map(({ user, userId }) => user ?? userId), ...notes.map(({ user, userId }) => user ?? userId),
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull), ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull), ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
]; ];
const packedUsers = await this.userEntityService.packMany(users, me) const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .then(users => new Map(users.map(u => [u.id, u])));

View File

@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js'; import { RoleEntityService } from './RoleEntityService.js';
@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit {
user, user,
reaction: reaction.reaction, reaction: reaction.reaction,
}; };
}))).filter(r => isNotNull(r.user)); }))).filter(r => r.user != null);
// if all users have been deleted, don't show this notification // if all users have been deleted, don't show this notification
if (reactions.length === 0) { if (reactions.length === 0) {
return null; return null;
@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit {
} }
return this.userEntityService.pack(userId, { id: meId }); return this.userEntityService.pack(userId, { id: meId });
}))).filter(isNotNull); }))).filter(x => x != null);
// if all users have been deleted, don't show this notification // if all users have been deleted, don't show this notification
if (users.length === 0) { if (users.length === 0) {
return null; return null;
@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit {
validNotifications = await this.#filterValidNotifier(validNotifications, meId); validNotifications = await this.#filterValidNotifier(validNotifications, meId);
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null);
const notes = noteIds.length > 0 ? await this.notesRepository.find({ const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) }, where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit {
); );
}); });
return (await Promise.all(packPromises)).filter(isNotNull); return (await Promise.all(packPromises)).filter(x => x != null);
} }
@bindThis @bindThis
@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit {
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)), this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]); ]);
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull); const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({ const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) }, where: { id: In(notifierIds) },
}) : []; }) : [];
@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit {
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => { const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers); const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null; return isValid ? notification : null;
}))) as [T | null] ).filter(isNotNull); }))) as [T | null] ).filter(x => x != null);
return filteredNotifications; return filteredNotifications;
} }

View File

@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
@ -106,7 +105,7 @@ export class PageEntityService {
script: page.script, script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)), attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
}); });

View File

@ -47,7 +47,6 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
@ -514,7 +513,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
.then(xs => xs.length === 0 ? null : xs.filter(isNotNull)) .then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
: null, : null,
createdAt: this.idService.parse(user.id).date.toISOString(), createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,

View File

@ -1,8 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
return input != null;
}

View File

@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
return Math.max(...xs); return Math.max(...xs);
} }
/**
* Splits an array based on the equivalence relation.
* The concatenation of the result is equal to the argument.
*/
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}
}
return groups;
}
/**
* Splits an array based on the equivalence relation induced by the function.
* The concatenation of the result is equal to the argument.
*/
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
return groupBy((a, b) => f(a) === f(b), xs);
}
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
return collections.reduce((obj: Record<string, T[]>, item: T) => {
const key = keySelector(item);
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = [];
}
obj[key].push(item);
return obj;
}, {});
}
/** /**
* Compare two arrays by lexicographical order * Compare two arrays by lexicographical order
*/ */

View File

@ -1,8 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function gcd(a: number, b: number): number {
return b === 0 ? a : gcd(b, a % b);
}

View File

@ -1,25 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface IMaybe<T> {
isJust(): this is IJust<T>;
}
export interface IJust<T> extends IMaybe<T> {
get(): T;
}
export function just<T>(value: T): IJust<T> {
return {
isJust: () => true,
get: () => value,
};
}
export function nothing<T>(): IMaybe<T> {
return {
isJust: () => false,
};
}

View File

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function concat(xs: string[]): string {
return xs.join('');
}
export function capitalize(s: string): string {
return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
}
export function toUpperCase(s: string): string {
return s.toUpperCase();
}
export function toLowerCase(s: string): string {
return s.toLowerCase();
}

View File

@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const fallback = Symbol('fallback');

View File

@ -82,34 +82,14 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
export interface MiRepository<T extends ObjectLiteral> { export interface MiRepository<T extends ObjectLiteral> {
createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[]; createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>; insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void; selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
} }
export const miRepository = { export const miRepository = {
createTableColumnNames(queryBuilder) { createTableColumnNames() {
// @ts-expect-error -- protected return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName);
const insertedColumns = queryBuilder.getInsertedColumns();
if (insertedColumns.length) {
return insertedColumns.map(column => column.databaseName);
}
if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
// @ts-expect-error -- protected
const valueSets = queryBuilder.getValueSets();
if (valueSets.length === 1) {
return Object.keys(valueSets[0]);
}
}
return queryBuilder.expressionMap.insertColumns;
},
createTableColumnNamesWithPrimaryKey(queryBuilder) {
const columnNames = this.createTableColumnNames(queryBuilder);
if (!columnNames.includes('id')) {
columnNames.unshift('id');
}
return columnNames;
}, },
async insertOne(entity, findOptions?) { async insertOne(entity, findOptions?) {
const queryBuilder = this.createQueryBuilder().insert().values(entity); const queryBuilder = this.createQueryBuilder().insert().values(entity);
@ -117,7 +97,7 @@ export const miRepository = {
const mainAlias = queryBuilder.expressionMap.mainAlias!; const mainAlias = queryBuilder.expressionMap.mainAlias!;
const name = mainAlias.name; const name = mainAlias.name;
mainAlias.name = 't'; mainAlias.name = 't';
const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder); const columnNames = this.createTableColumnNames();
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames }); const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -138,7 +118,7 @@ export const miRepository = {
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
return builder.select(selection, selectionAliasName); return builder.select(selection, selectionAliasName);
}; };
for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) { for (const columnName of this.createTableColumnNames()) {
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
} }
}, },

View File

@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -109,6 +109,12 @@ export class DeliverProcessorService {
suspensionState: 'autoSuspendedForNotResponding', suspensionState: 'autoSuspendedForNotResponding',
}); });
} }
} else {
// isNotRespondingがtrueでnotRespondingSinceがnullの場合はnotRespondingSinceをセット
// notRespondingSinceは新たな機能なので、それ以前のデータにはnotRespondingSinceがない場合がある
this.federatedInstanceService.update(i.id, {
notRespondingSince: new Date(),
});
} }
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();

View File

@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
} }
statusCode = statusCode ?? 403; statusCode = statusCode ?? 403;
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
const info: unknown = err.info;
const unixEpochInSeconds = Date.now();
if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
} else {
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
}
} else if (!statusCode) { } else if (!statusCode) {
statusCode = 500; statusCode = 500;
} }
@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) { if (factor > 0) {
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
if ('info' in err) {
// errはLimiter.LimiterInfoであることが期待される
throw new ApiError({ throw new ApiError({
message: 'Rate limit exceeded. Please try again later.', message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED', code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429, httpStatusCode: 429,
}); }, err.info);
} else {
throw new TypeError('information must be a rate-limiter information.');
}
}); });
} }
} }

View File

@ -32,11 +32,13 @@ export class RateLimiterService {
@bindThis @bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) { public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((ok, reject) => { {
if (this.disabled) ok(); if (this.disabled) {
return Promise.resolve();
}
// Short-term limit // Short-term limit
const min = (): void => { const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor, duration: limitation.minInterval! * factor,
@ -46,25 +48,25 @@ export class RateLimiterService {
minIntervalLimiter.get((err, info) => { minIntervalLimiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject({ code: 'ERR', info });
} }
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL'); return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else { } else {
if (hasLongTermLimit) { if (hasLongTermLimit) {
max(); return max.then(ok, reject);
} else { } else {
ok(); return ok();
} }
} }
}); });
}; });
// Long term limit // Long term limit
const max = (): void => { const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor, duration: limitation.duration! * factor,
@ -74,18 +76,18 @@ export class RateLimiterService {
limiter.get((err, info) => { limiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject({ code: 'ERR', info });
} }
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED'); return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else { } else {
ok(); return ok();
} }
}); });
}; });
const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasShortTermLimit = typeof limitation.minInterval === 'number';
@ -94,12 +96,12 @@ export class RateLimiterService {
typeof limitation.max === 'number'; typeof limitation.max === 'number';
if (hasShortTermLimit) { if (hasShortTermLimit) {
min(); return min;
} else if (hasLongTermLimit) { } else if (hasLongTermLimit) {
max(); return max;
} else { } else {
ok(); return Promise.resolve();
} }
}); }
} }
} }

View File

@ -40,7 +40,7 @@ export const paramDef = {
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' }, dayOfWeek: { type: 'integer' },
}, },
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], required: ['id'],
} as const; } as const;
@Injectable() @Injectable()
@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio, ratio: ps.ratio,
memo: ps.memo, memo: ps.memo,
imageUrl: ps.imageUrl, imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt), expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: new Date(ps.startsAt), startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek, dayOfWeek: ps.dayOfWeek,
}); });

View File

@ -61,7 +61,7 @@ export const meta = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js'; import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
@ -50,19 +49,6 @@ export const paramDef = {
}, },
required: [ required: [
'roleId', 'roleId',
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'displayOrder',
'policies',
], ],
} as const; } as const;

View File

@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentAntennasCount = await this.antennasRepository.countBy({ const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ClipService } from '@/core/ClipService.js'; import { ClipService } from '@/core/ClipService.js';
@ -41,7 +41,7 @@ export const paramDef = {
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
}, },
required: ['clipId', 'name'], required: ['clipId'],
} as const; } as const;
@Injectable() @Injectable()

View File

@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check if the circular reference will occur // Check if the circular reference will occur
const checkCircle = async (folderId: string): Promise<boolean> => { const checkCircle = async (folderId: string): Promise<boolean> => {
// Fetch folder const folder2 = await this.driveFoldersRepository.findOneByOrFail({
const folder2 = await this.driveFoldersRepository.findOneBy({
id: folderId, id: folderId,
}); });
if (folder2!.id === folder!.id) { if (folder2.id === folder.id) {
return true; return true;
} else if (folder2!.parentId) { } else if (folder2.parentId) {
return await checkCircle(folder2!.parentId); return await checkCircle(folder2.parentId);
} else { } else {
return false; return false;
} }

View File

@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -70,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId, id: fileId,
userId: me.id, userId: me.id,
}), }),
))).filter(isNotNull); ))).filter(x => x != null);
if (files.length === 0) { if (files.length === 0) {
throw new Error(); throw new Error();

View File

@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -48,7 +47,7 @@ export const paramDef = {
} }, } },
isSensitive: { type: 'boolean', default: false }, isSensitive: { type: 'boolean', default: false },
}, },
required: ['postId', 'title', 'fileIds'], required: ['postId'],
} as const; } as const;
@Injectable() @Injectable()
@ -63,16 +62,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private galleryPostEntityService: GalleryPostEntityService, private galleryPostEntityService: GalleryPostEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const files = (await Promise.all(ps.fileIds.map(fileId => let files: Array<MiDriveFile> | undefined;
if (ps.fileIds) {
files = (await Promise.all(ps.fileIds.map(fileId =>
this.driveFilesRepository.findOneBy({ this.driveFilesRepository.findOneBy({
id: fileId, id: fileId,
userId: me.id, userId: me.id,
}), }),
))).filter(isNotNull); ))).filter(x => x != null);
if (files.length === 0) { if (files.length === 0) {
throw new Error(); throw new Error();
} }
}
await this.galleryPostsRepository.update({ await this.galleryPostsRepository.update({
id: ps.postId, id: ps.postId,
@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
title: ps.title, title: ps.title,
description: ps.description, description: ps.description,
isSensitive: ps.isSensitive, isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id), fileIds: files ? files.map(file => file.id) : undefined,
}); });
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });

View File

@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (file.size === 0) throw new ApiError(meta.errors.emptyFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }
this.queueService.createImportAntennasJob(me, antennas); this.queueService.createImportAntennasJob(me, antennas);

View File

@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentWebhooksCount = await this.webhooksRepository.countBy({ const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks); throw new ApiError(meta.errors.tooManyWebhooks);
} }

View File

@ -34,13 +34,13 @@ export const paramDef = {
webhookId: { type: 'string', format: 'misskey:id' }, webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 }, url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', maxLength: 1024, default: '' }, secret: { type: 'string', nullable: true, maxLength: 1024 },
on: { type: 'array', items: { on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes, type: 'string', enum: webhookEventTypes,
} }, } },
active: { type: 'boolean' }, active: { type: 'boolean' },
}, },
required: ['webhookId', 'name', 'url', 'on', 'active'], required: ['webhookId'],
} as const; } as const;
// TODO: ロジックをサービスに切り出す // TODO: ロジックをサービスに切り出す
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.webhooksRepository.update(webhook.id, { await this.webhooksRepository.update(webhook.id, {
name: ps.name, name: ps.name,
url: ps.url, url: ps.url,
secret: ps.secret, secret: ps.secret === null ? '' : ps.secret,
on: ps.on, on: ps.on,
active: ps.active, active: ps.active,
}); });

View File

@ -36,6 +36,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED', code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
}, },
cannotReactToRenote: {
message: 'You cannot react to Renote.',
code: 'CANNOT_REACT_TO_RENOTE',
id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
},
}, },
} as const; } as const;
@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.reactionService.create(me, note, ps.reaction).catch(err => { await this.reactionService.create(me, note, ps.reaction).catch(err => {
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
throw err; throw err;
}); });
return; return;

View File

@ -70,7 +70,7 @@ export const paramDef = {
alignCenter: { type: 'boolean' }, alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' },
}, },
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], required: ['pageId'],
} as const; } as const;
@Injectable() @Injectable()
@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied); throw new ApiError(meta.errors.accessDenied);
} }
let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) { if (ps.eyeCatchingImageId != null) {
eyeCatchingImage = await this.driveFilesRepository.findOneBy({ const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
id: ps.eyeCatchingImageId, id: ps.eyeCatchingImageId,
userId: me.id, userId: me.id,
}); });
@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.pagesRepository.update(page.id, { await this.pagesRepository.update(page.id, {
updatedAt: new Date(), updatedAt: new Date(),
title: ps.title, title: ps.title,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: ps.name,
name: ps.name === undefined ? page.name : ps.name,
summary: ps.summary === undefined ? page.summary : ps.summary, summary: ps.summary === undefined ? page.summary : ps.summary,
content: ps.content, content: ps.content,
variables: ps.variables, variables: ps.variables,
script: ps.script, script: ps.script,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing alignCenter: ps.alignCenter,
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing font: ps.font,
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, eyeCatchingImageId: ps.eyeCatchingImageId,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
font: ps.font === undefined ? page.font : ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId === null
? null
: ps.eyeCatchingImageId === undefined
? page.eyeCatchingImageId
: eyeCatchingImage!.id,
}); });
}); });
} }

View File

@ -12,7 +12,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(), host: acct.host ?? IsNull(),
}))); })));
return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' }); return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -100,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }

View File

@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }

View File

@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MfmService } from "@/core/MfmService.js";
import { parse as mfmParse } from 'mfm-js';
@Injectable() @Injectable()
export class FeedService { export class FeedService {
@ -33,6 +35,7 @@ export class FeedService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private idService: IdService, private idService: IdService,
private mfmService: MfmService,
) { ) {
} }
@ -76,13 +79,14 @@ export class FeedService {
id: In(note.fileIds), id: In(note.fileIds),
}) : []; }) : [];
const file = files.find(file => file.type.startsWith('image/')); const file = files.find(file => file.type.startsWith('image/'));
const text = note.text;
feed.addItem({ feed.addItem({
title: `New note by ${author.name}`, title: `New note by ${author.name}`,
link: `${this.config.url}/notes/${note.id}`, link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date, date: this.idService.parse(note.id).date,
description: note.cw ?? undefined, description: note.cw ?? undefined,
content: note.text ?? undefined, content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined, image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
}); });
} }

View File

@ -29,7 +29,8 @@
let forceError = localStorage.getItem('forceError'); let forceError = localStorage.getItem('forceError');
if (forceError != null) { if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return;
} }
//#region Detect language & fetch translations //#region Detect language & fetch translations
@ -155,7 +156,12 @@
document.head.appendChild(css); document.head.appendChild(css);
} }
function renderError(code, details) { async function renderError(code, details) {
// Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
let errorsElement = document.getElementById('errors'); let errorsElement = document.getElementById('errors');
if (!errorsElement) { if (!errorsElement) {
@ -314,6 +320,6 @@
#errorInfo { #errorInfo {
width: 50%; width: 50%;
} }
`) }`)
} }
})(); })();

View File

@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@ -0,0 +1,43 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../../shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@ -1,11 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['../.eslintrc.cjs'],
env: {
node: true,
jest: true,
},
};

View File

@ -1,5 +1,3 @@
version: "3"
services: services:
redistest: redistest:
image: redis:7 image: redis:7

View File

@ -163,8 +163,7 @@ describe('アンテナ', () => {
}); });
test('が上限いっぱいまで作成できること', async () => { test('が上限いっぱいまで作成できること', async () => {
// antennaLimit + 1まで作れるのがキモ const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({
const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
endpoint: 'antennas/create', endpoint: 'antennas/create',
parameters: { ...defaultParam }, parameters: { ...defaultParam },
user: alice, user: alice,

View File

@ -153,8 +153,7 @@ describe('クリップ', () => {
}); });
test('の作成はポリシーで定められた数以上はできない。', async () => { test('の作成はポリシーで定められた数以上はできない。', async () => {
// ポリシー + 1まで作れるという所がミソ const clipLimit = DEFAULT_POLICIES.clipLimit;
const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
for (let i = 0; i < clipLimit; i++) { for (let i = 0; i < clipLimit; i++) {
await create(); await create();
} }
@ -327,7 +326,7 @@ describe('クリップ', () => {
}); });
test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => { test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => {
const clipLimit = DEFAULT_POLICIES.clipLimit + 1; const clipLimit = DEFAULT_POLICIES.clipLimit;
const clips = await createMany({}, clipLimit); const clips = await createMany({}, clipLimit);
const res = await list({ const res = await list({
parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる
@ -705,7 +704,7 @@ describe('クリップ', () => {
// TODO: 17000msくらいかかる... // TODO: 17000msくらいかかる...
test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => { test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`, text: `test ${i}`,
}) as unknown)) as Misskey.entities.Note[]; }) as unknown)) as Misskey.entities.Note[];

View File

@ -23,7 +23,7 @@ describe('Drive', () => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'; const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg';
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
alice, alice,
@ -41,14 +41,14 @@ describe('Drive', () => {
const file = await catcher; const file = await catcher;
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg'); assert.strictEqual(file.name, '192.jpg');
assert.strictEqual(file.type, 'image/jpeg'); assert.strictEqual(file.type, 'image/jpeg');
}); });
test('ローカルからアップロードできる', async () => { test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body.type, 'image/jpeg'); assert.strictEqual(res.body.type, 'image/jpeg');

View File

@ -266,6 +266,67 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('リノートにリアクションできない', async () => {
const bobNote = await post(bob, { text: 'hi' });
const bobRenote = await post(bob, { renoteId: bobNote.id });
const res = await api('notes/reactions/create', {
noteId: bobRenote.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE');
});
test('引用にリアクションできる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id });
const res = await api('notes/reactions/create', {
noteId: bobRenote.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
});
test('空文字列のリアクションは\u2764にフォールバックされる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const res = await api('notes/reactions/create', {
noteId: bobNote.id,
reaction: '',
}, alice);
assert.strictEqual(res.status, 204);
const reaction = await api('notes/reactions', {
noteId: bobNote.id,
});
assert.strictEqual(reaction.body.length, 1);
assert.strictEqual(reaction.body[0].type, '\u2764');
});
test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const res = await api('notes/reactions/create', {
noteId: bobNote.id,
reaction: 'Hello!',
}, alice);
assert.strictEqual(res.status, 204);
const reaction = await api('notes/reactions', {
noteId: bobNote.id,
});
assert.strictEqual(reaction.body.length, 1);
assert.strictEqual(reaction.body[0].type, '\u2764');
});
test('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
// @ts-expect-error param must not be empty // @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice); const res = await api('notes/reactions/create', {}, alice);
@ -523,7 +584,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body!.name, 'Lenna.jpg'); assert.strictEqual(res.body!.name, '192.jpg');
}); });
test('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {

View File

@ -153,6 +153,23 @@ describe('Webリソース', () => {
path: path('nonexisting'), path: path('nonexisting'),
status: 404, status: 404,
})); }));
describe(' has entry such ', () => {
beforeEach(() => {
post(alice, { text: "**a**" })
});
test('MFMを含まない。', async () => {
const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
const _body: unknown = content.body;
// JSONフィードのときは改めて文字列化する
const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
if (body.includes("**a**")) {
throw new Error("MFM shouldn't be included");
}
});
})
}); });
describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {

View File

@ -7,12 +7,13 @@ import { INestApplicationContext } from '@nestjs/common';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
import * as assert from 'assert'; import * as assert from 'assert';
import { loadConfig } from '@/config.js'; import { loadConfig } from '@/config.js';
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { jobQueue } from '@/boot/common.js'; import { jobQueue } from '@/boot/common.js';
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Account Move', () => { describe('Account Move', () => {
@ -271,7 +272,7 @@ describe('Account Move', () => {
assert.strictEqual(move.status, 200); assert.strictEqual(move.status, 200);
await sleep(1000 * 3); // wait for jobs to finish await setTimeout(1000 * 3); // wait for jobs to finish
// Unfollow delayed? // Unfollow delayed?
const aliceFollowings = await api('users/following', { const aliceFollowings = await api('users/following', {
@ -330,7 +331,7 @@ describe('Account Move', () => {
}); });
test('Unfollowed after 10 sec (24 hours in production).', async () => { test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8); await setTimeout(1000 * 8);
const following = await api('users/following', { const following = await api('users/following', {
userId: alice.id, userId: alice.id,

View File

@ -41,7 +41,7 @@ describe('Note', () => {
}); });
test('ファイルを添付できる', async () => { test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const res = await api('notes/create', { const res = await api('notes/create', {
fileIds: [file.id], fileIds: [file.id],
@ -53,7 +53,7 @@ describe('Note', () => {
}, 1000 * 10); }, 1000 * 10);
test('他人のファイルで怒られる', async () => { test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const res = await api('notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',

View File

@ -6,7 +6,8 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, signup, sleep, waitFire } from '../utils.js'; import { setTimeout } from 'node:timers/promises';
import { api, post, signup, waitFire } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Renote Mute', () => { describe('Renote Mute', () => {
@ -35,7 +36,7 @@ describe('Renote Mute', () => {
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
@ -52,7 +53,7 @@ describe('Renote Mute', () => {
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
@ -69,7 +70,7 @@ describe('Renote Mute', () => {
const bobRenote = await post(bob, { renoteId: carolNote.id }); const bobRenote = await post(bob, { renoteId: carolNote.id });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);

View File

@ -7,17 +7,26 @@
// pnpm jest -- e2e/timelines.ts // pnpm jest -- e2e/timelines.ts
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; import { setTimeout } from 'node:timers/promises';
import { Redis } from 'ioredis';
import { loadConfig } from '@/config.js';
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
function genHost() { function genHost() {
return randomString() + '.example.com'; return randomString() + '.example.com';
} }
function waitForPushToTl() { function waitForPushToTl() {
return sleep(500); return setTimeout(500);
} }
let redisForTimelines: Redis;
describe('Timelines', () => { describe('Timelines', () => {
beforeAll(() => {
redisForTimelines = new Redis(loadConfig().redisForTimelines);
});
describe('Home TL', () => { describe('Home TL', () => {
test.concurrent('自分の visibility: followers なノートが含まれる', async () => { test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
const [alice] = await Promise.all([signup()]); const [alice] = await Promise.all([signup()]);
@ -36,7 +45,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
@ -52,7 +61,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
@ -69,7 +78,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -86,7 +95,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -103,7 +112,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
@ -120,7 +129,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -139,7 +148,7 @@ describe('Timelines', () => {
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: carol.id }, bob);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -158,7 +167,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
@ -174,7 +183,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
@ -190,7 +199,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -220,7 +229,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: carolNote.id });
@ -236,7 +245,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: carolNote.id });
@ -254,7 +263,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -272,7 +281,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -287,7 +296,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -305,7 +314,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -351,7 +360,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const [bobFile, carolFile] = await Promise.all([ const [bobFile, carolFile] = await Promise.all([
uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
@ -376,7 +385,7 @@ describe('Timelines', () => {
const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl(); await waitForPushToTl();
@ -403,7 +412,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -430,7 +439,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -558,7 +567,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
@ -574,7 +583,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
@ -591,7 +600,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -609,7 +618,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -625,7 +634,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -695,7 +704,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -709,7 +718,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -812,7 +821,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl(); await waitForPushToTl();
@ -827,7 +836,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -842,7 +851,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -857,7 +866,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -873,7 +882,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
@ -891,7 +900,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -908,7 +917,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -925,7 +934,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -942,7 +951,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -958,7 +967,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -974,7 +983,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); await api('users/lists/push', { listId: list.id, userId: alice.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -991,7 +1000,7 @@ describe('Timelines', () => {
const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl(); await waitForPushToTl();
@ -1023,7 +1032,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -1040,7 +1049,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); await api('users/lists/push', { listId: list.id, userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -1080,7 +1089,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -1220,7 +1229,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -1235,7 +1244,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('mute/create', { userId: bob.id }, alice); await api('mute/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
@ -1272,6 +1281,33 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
}); });
/** @see https://github.com/misskey-dev/misskey/issues/14000 */
test.concurrent('FTT: sinceId にキャッシュより古いートを指定しても、sinceId による絞り込みが正しく動作する', async () => {
const alice = await signup();
const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
const note1 = await post(alice, { text: '1' });
const note2 = await post(alice, { text: '2' });
await redisForTimelines.del('list:userTimeline:' + alice.id);
const note3 = await post(alice, { text: '3' });
const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id });
assert.deepStrictEqual(res.body, [note1, note2, note3]);
});
test.concurrent('FTT: sinceId にキャッシュより古いートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => {
const alice = await signup();
const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
const note1 = await post(alice, { text: '1' });
const note2 = await post(alice, { text: '2' });
await redisForTimelines.del('list:userTimeline:' + alice.id);
const note3 = await post(alice, { text: '3' });
const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' });
await post(alice, { text: '4' });
const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id });
assert.deepStrictEqual(res.body, [note3, note2, note1]);
});
}); });
// TODO: リノートミュート済みユーザーのテスト // TODO: リノートミュート済みユーザーのテスト

View File

@ -17,8 +17,8 @@ describe('users/notes', () => {
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png');
jpgNote = await post(alice, { jpgNote = await post(alice, {
fileIds: [jpg.id], fileIds: [jpg.id],
}); });

View File

@ -0,0 +1,22 @@
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../../shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
},
];

View File

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as assert from 'assert';
import { just, nothing } from '../../src/misc/prelude/maybe.js';
describe('just', () => {
test('has a value', () => {
assert.deepStrictEqual(just(3).isJust(), true);
});
test('has the inverse called get', () => {
assert.deepStrictEqual(just(3).get(), 3);
});
});
describe('nothing', () => {
test('has no value', () => {
assert.deepStrictEqual(nothing().isJust(), false);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

View File

@ -83,21 +83,21 @@ describe('FileInfoService', () => {
describe('IMAGE', () => { describe('IMAGE', () => {
test('Generic JPEG', async () => { test('Generic JPEG', async () => {
const path = `${resources}/Lenna.jpg`; const path = `${resources}/192.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
delete info.porn; delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 25360, size: 5131,
md5: '091b3f259662aa31e2ffef4519951168', md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
type: { type: {
mime: 'image/jpeg', mime: 'image/jpeg',
ext: 'jpg', ext: 'jpg',
}, },
width: 512, width: 192,
height: 512, height: 192,
orientation: undefined, orientation: undefined,
}); });
}); });

View File

@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock'; import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
@ -29,7 +30,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js'; import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -278,7 +278,7 @@ describe('RoleService', () => {
// ストリーミング経由で反映されるまでちょっと待つ // ストリーミング経由で反映されるまでちょっと待つ
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const resultAfter25hAgain = await roleService.getUserPolicies(user.id); const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
@ -807,7 +807,7 @@ describe('RoleService', () => {
await roleService.assign(user.id, role.id); await roleService.assign(user.id, role.id);
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const assignments = await roleAssignmentsRepository.find({ const assignments = await roleAssignmentsRepository.find({
where: { where: {
@ -835,7 +835,7 @@ describe('RoleService', () => {
await roleService.assign(user.id, role.id); await roleService.assign(user.id, role.id);
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const assignments = await roleAssignmentsRepository.find({ const assignments = await roleAssignmentsRepository.find({
where: { where: {

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { setTimeout } from 'node:timers/promises';
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
@ -16,7 +17,7 @@ import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString, sleep } from '../utils.js'; import { randomString } from '../utils.js';
describe('SystemWebhookService', () => { describe('SystemWebhookService', () => {
let app: TestingModule; let app: TestingModule;
@ -358,7 +359,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook]); expect(fetchedWebhooks).toEqual([webhook]);
@ -377,7 +378,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([]); expect(fetchedWebhooks).toEqual([]);
@ -407,7 +408,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]); expect(fetchedWebhooks).toEqual([webhook2]);
@ -434,7 +435,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);
@ -457,7 +458,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]); expect(fetchedWebhooks).toEqual([webhook2]);
@ -481,7 +482,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);
@ -504,7 +505,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);

Some files were not shown because too many files have changed in this diff Show More