Compare commits

..

81 Commits

Author SHA1 Message Date
syuilo 24705a7e39 Merge branch 'develop' 2023-02-04 09:12:26 +09:00
syuilo 8add8025a0 13.3.1 2023-02-04 09:12:18 +09:00
syuilo 32fa79d928 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-04 09:11:36 +09:00
syuilo 534be6ff25 Update CHANGELOG.md 2023-02-04 09:11:33 +09:00
syuilo f684c07567 New Crowdin updates (#9789)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)
2023-02-04 09:10:16 +09:00
syuilo 788ae2f6ca fix(client): validate urls to improve security 2023-02-04 09:10:01 +09:00
syuilo 572000f868 clean up 2023-02-04 09:01:26 +09:00
Rox Squires 57f5df2d22 Fix | Vue-plyr CORS issue (#9790)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

For some reason on the dependencies there was to instances of vue-plyr

* Added MkMediaAudio

* Update MkMediaList.vue

* CORS checks

* Update MkMediaVideo.vue

* Update MkMediaVideo.vue

* Fixed CORS

the property made the video player use the CORS policy that stopped instance not using media caching not able to load the video from remote instance

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-04 09:00:16 +09:00
tamaina b2a67ba5ca fix(client): オートコンプリートでUnicode絵文字がカスタム絵文字として表示されてしまうのを修正 2023-02-03 21:21:36 +00:00
tamaina d78e15cc1a fix(client): カスタム絵文字にアニメーション画像を再生しない設定が適用されていない問題を修正 2023-02-03 20:37:15 +00:00
syuilo ceab34f5f3 Merge branch 'develop' 2023-02-03 17:57:40 +09:00
syuilo 3a62625bbc 13.3.0 2023-02-03 17:57:27 +09:00
Roxy Squires ad6844ac4a Bug | Fixed the error when running pnpm i --frozen-lockfile (#9782)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

For some reason on the dependencies there was to instances of vue-plyr

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-03 17:46:16 +09:00
syuilo a8c252a613 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-03 17:44:28 +09:00
syuilo 1d39f785f1 perf: use replaceAll instead of regex 2023-02-03 17:44:25 +09:00
Roxy Squires 4b8b29b862 enhance - Added vue-plyr as the standard video player (#9766)
* Added Video player

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-02-03 17:15:25 +09:00
tamaina 0d148bd23b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-03 08:07:25 +00:00
tamaina ebedb81e3f update idb-proxy.ts
Maybe fixed #9769
2023-02-03 08:07:17 +00:00
syuilo d195406fdc New Crowdin updates (#9760)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)
2023-02-03 15:14:02 +09:00
syuilo 5173ed37f9 Update CHANGELOG.md 2023-02-03 15:13:28 +09:00
syuilo 825551d64f drop syslog
Close #9774
2023-02-03 15:08:36 +09:00
syuilo 449761bada Update CHANGELOG.md 2023-02-03 15:06:24 +09:00
syuilo 5859df389f Create 1675404035646-cleanup.js 2023-02-03 15:02:54 +09:00
syuilo 562b02310f drop twitter/github/discord integrations
Close #9775
2023-02-03 15:01:31 +09:00
syuilo 65ed702d87 update deps 2023-02-03 14:44:09 +09:00
syuilo c559a9843f drop hashtag chart 2023-02-03 14:10:14 +09:00
syuilo 88c3957085 enhance(client): hidden ads when canHideAds is true 2023-02-03 14:03:34 +09:00
Masaya Suzuki 01778e11dc CONTRIBUTING: テストが配置されている場所の記述修正 (#9772) 2023-02-03 03:11:50 +09:00
Masaya Suzuki 9d9e8a3c4e CONTRIBUTING: yarn -> pnpm (#9771) 2023-02-03 03:11:26 +09:00
syuilo ed3e035ad6 refactor: use test 2023-02-02 18:18:25 +09:00
syuilo 07f885fea8 refactor 2023-02-02 18:08:34 +09:00
syuilo 2cc98226ca improve RoleService test 2023-02-02 18:06:23 +09:00
tamaina 8a6f73c5ff enhance: PizzaxデータをindexedDBに保存するように (#9225)
* Revert "Revert #8098"

This reverts commit 8b9dc962ae.

* fix

* use deepClone instead of deepclone

* defaultStore.loaded

* fix load

* wait ready

* use top-level await, await in device-kind.ts
2023-02-02 16:43:56 +09:00
syuilo 00e3453ce1 improve role test 2023-02-02 14:28:29 +09:00
syuilo 16646dd77a Update README.md 2023-02-02 10:31:13 +09:00
syuilo 1f39d1fe26 test: add test of RoleService 2023-02-02 10:26:59 +09:00
syuilo e8f3c587c9 Update pnpm-lock.yaml 2023-02-02 10:26:43 +09:00
syuilo 4b43745e7c fix(test): add @jest/globals 2023-02-02 10:26:29 +09:00
syuilo 9db2f60053 refactor(client): use top-level await 2023-02-02 09:00:34 +09:00
syuilo 4610d8dfe3 refactor: fix type 2023-02-01 20:15:11 +09:00
syuilo fa296efdf6 refactor: fix type 2023-02-01 20:13:22 +09:00
syuilo d9d98f84bf refactor: fix type 2023-02-01 20:12:42 +09:00
tamaina 7c3143b8e5 enhance(backend): enhance SchemaType handling of anyOf (#9762)
* enhance(backend): enhance anyOf handling

* clean up
2023-02-01 20:04:01 +09:00
syuilo 387fcd5c5d refactor: fix type 2023-02-01 17:29:28 +09:00
syuilo ebc6437977 refactor: tweak variable name 2023-02-01 16:24:50 +09:00
syuilo dbc23b5d20 Merge branch 'develop' 2023-02-01 11:29:30 +09:00
syuilo 843f1aed4f 13.2.6 2023-02-01 11:29:17 +09:00
syuilo e42938cad6 Update CHANGELOG.md 2023-02-01 11:27:37 +09:00
YS 2a41f6c383 enhance: Unicode絵文字名逆引き効率化 (#9757)
* Unicode絵文字名前取得を連想配列で行う

* Unicode絵文字事前カテゴリ集計

* Mapを使用

* Update packages/frontend/src/scripts/emojilist.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-02-01 11:25:13 +09:00
syuilo 671d21a2c1 New Crowdin updates (#9737)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
2023-02-01 11:21:58 +09:00
syuilo 515692d7a6 update aiscript 2023-02-01 11:20:28 +09:00
Nya Candy 00d28826b9 fix(try): ld signature normalizer (#9758) 2023-01-31 19:37:39 +09:00
tamaina 5b38f76254 update CHANGELOG.md 2023-01-28 12:37:28 +00:00
tamaina ca7dbd6010 gitignore docker-compose.yml 2023-01-28 11:34:34 +00:00
tamaina 133644e5a9 Rename docker-compose.yml to docker-compose.yml.example 2023-01-28 11:33:44 +00:00
tamaina 04d60426c7 modify CHANGELOG.md 2023-01-28 06:22:38 +00:00
tamaina 8282bbd07c fix(client): Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう
Fix #9598
2023-01-28 06:15:29 +00:00
yupix 7190bd00c9 feat: classicモードでテーマが自動変更された際元に戻すように (#9669)
* feat: classicモードでテーマが自動変更された際元に戻すように

* docs: update CHANGELOG.md

* fix: prefixを miux:ui_temp から ui_temp に変更
2023-01-27 13:52:51 +09:00
syuilo 44b9539818 Merge branch 'develop' 2023-01-27 12:33:36 +09:00
syuilo b2ed4c9508 13.2.5 2023-01-27 12:33:20 +09:00
syuilo c7b5c8b19e swがビルドできないのを修正 2023-01-27 12:33:15 +09:00
syuilo f4bee24ccf Merge branch 'develop' 2023-01-27 11:44:14 +09:00
syuilo e9cb18c5aa 13.2.4 2023-01-27 11:44:04 +09:00
syuilo d8f33bc0af update deps 2023-01-27 11:40:18 +09:00
syuilo 663999556f New Crowdin updates (#9734)
* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
2023-01-27 11:35:27 +09:00
syuilo c5a12ca2c7 fix(client): フォロー申請・フォローのボタンが、通知から消えている問題を修正
Fix #9717
2023-01-27 11:35:04 +09:00
Takuya Yoshida 7af0e38dd3 Use cache on build (#9639) 2023-01-27 11:30:22 +09:00
syuilo 7d9d1ae7c2 enhance(client): tweak custom emoji cache 2023-01-27 11:28:51 +09:00
syuilo cef448f0f2 tweak blur setting 2023-01-27 11:18:44 +09:00
syuilo 67d64c9365 refactor 2023-01-27 11:16:22 +09:00
syuilo 269af9d6b9 fix(client): ダッシュボードでオンラインユーザー数が表示されない問題を修正 2023-01-27 11:11:56 +09:00
syuilo d37a734379 fix(server): fix aggregation of retention 2023-01-27 11:10:37 +09:00
syuilo 7cb13cf839 proxyRemoteFilesがfalseならリモートカスタム絵文字は直リンにする 2023-01-26 18:44:43 +09:00
syuilo d7dda8f6e3 絵文字ピッカーでカスタム絵文字が表示されないのを修正 2023-01-26 18:28:17 +09:00
tamaina 6670c72f8b fix(client): note reacted reflection failed
Fix #9730
2023-01-26 08:48:36 +00:00
hayabusa b21064ffa4 リアクション履歴が公開なら、ログインしていなくても表示できるように (#9728) 2023-01-26 16:10:32 +09:00
Kagami Sascha Rosylight 1959cb462b Default to animation: false when prefers-reduced-motion is set (#9690)
* Default to `animation: false` when prefers-reduced-motion is set

* `.matches`
2023-01-26 16:08:45 +09:00
Kagami Sascha Rosylight 1d6767ef0c Try reinstalling cypress in CI (#9694) 2023-01-26 16:07:15 +09:00
tamaina 4735ae6451 refactor: /proxyをFileServerServiceに統合し、/proxyのurlで/filesが指定されていた場合は直接ファイルを解決するようにする (#9709)
* wip?

* clean up

* Implement? HttpFetchService

* ✌️

* remove node-fetch

* fix

* refactor

* fix

* gateway timeout

* UndiciFetcherクラスを追加 (仮コミット, ビルドもstartもさせていない)

* fix

* add logger and fix url preview

* fix ip check

* enhance logger and error handling

* fix

* fix

* clean up

* Use custom fetcher for ApRequest / ApResolver

* bypassProxyはproxyBypassHostsに判断を委譲するように

* set maxRedirections (default 3, ApRequest/ApResolver: 0)

* fix

* wip????

* wip

* ✌️

* set .node-version

* clean up

* refactor

* clean up

* refactor

* refactor detectRequestType

* rename detectResponseType

* ✌️

* fix

* wip

* clean up

* no got

* remove got

* wip

* ✌️

* fix

* clean up

* remove unnnecessary const

* good cleanup

* no stream

* Revert "no stream"

This reverts commit 636f9192fc.

* fix

* cache-control: max-age=300 to error

* refactor cleanup
2023-01-26 16:06:29 +09:00
syuilo 452bd6db25 tweak custom emoji handling
Close #9721
2023-01-26 15:48:12 +09:00
syuilo f5d6b84381 chore: check emoji host 2023-01-26 14:29:28 +09:00
154 changed files with 4247 additions and 4665 deletions
-5
View File
@@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128
-5
View File
@@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128
+2
View File
@@ -31,3 +31,5 @@ jobs:
push: true push: true
tags: misskey/misskey:develop tags: misskey/misskey:develop
labels: develop labels: develop
cache-from: type=gha
cache-to: type=gha,mode=max
+5 -1
View File
@@ -109,8 +109,12 @@ jobs:
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091 # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
- name: ALSA Env - name: ALSA Env
run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
# XXX: This tries reinstalling Cypress if the binary is not cached
# Remove this when the cache issue is fixed
- name: Cypress install
run: pnpm exec cypress install
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@v4 uses: cypress-io/github-action@v5
with: with:
install: false install: false
start: pnpm start:test start: pnpm start:test
+1
View File
@@ -32,6 +32,7 @@ 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
# misskey # misskey
/build /build
+1 -1
View File
@@ -1 +1 @@
v18.12.1 v18.13.0
+43
View File
@@ -9,6 +9,49 @@
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 13.3.1 (2023/02/04)
### Bugfixes
- Client: カスタム絵文字にアニメーション画像を再生しない設定が適用されていない問題を修正
- Client: オートコンプリートでUnicode絵文字がカスタム絵文字として表示されてしまうのを修正
- Client: Fix Vue-plyr CORS issue
- Client: validate urls to improve security
## 13.3.0 (2023/02/03)
### Changes
- twitter/github/discord連携機能が削除されました
- ハッシュタグごとのチャートが削除されました
- syslogのサポートが削除されました
### Improvements
- ロールで広告の非表示が有効になっている場合は最初から広告を非表示にするように
## 13.2.6 (2023/02/01)
### Changes
- docker-compose.ymlをdocker-compose.yml.exampleにしました。docker-compose.ymlとしてコピーしてから使用してください。
### Improvements
- 絵文字ピッカーのパフォーマンスを改善
- AiScriptを0.12.4に更新
### Bugfixes
- Server: リレーと通信できない問題を修正
- Client: classicモード使用時にwindowサイズによってdefaultに変更された後に、windowサイズが元に戻ったらclassicに戻すように修正 #9669
- Client: Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう問題を修正
## 13.2.4 (2023/01/27)
### Improvements
- リモートカスタム絵文字表示時のパフォーマンスを改善
- Default to `animation: false` when prefers-reduced-motion is set
- リアクション履歴が公開なら、ログインしていなくても表示できるように
- tweak blur setting
- tweak custom emoji cache
### Bugfixes
- fix aggregation of retention
- ダッシュボードでオンラインユーザー数が表示されない問題を修正
- フォロー申請・フォローのボタンが、通知から消えている問題を修正
## 13.2.3 (2023/01/26) ## 13.2.3 (2023/01/26)
### Improvements ### Improvements
- カスタム絵文字の更新をリアルタイムで反映するように - カスタム絵文字の更新をリアルタイムで反映するように
+8 -8
View File
@@ -44,7 +44,7 @@ Thank you for your PR! Before creating a PR, please check the following:
- Check if there are any documents that need to be created or updated due to this change. - Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible. - If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance. - Please make sure that tests and Lint are passed in advance.
- You can run it with `yarn test` and `yarn lint`. [See more info](#testing) - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
- If this PR includes UI changes, please attach a screenshot in the text. - If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
@@ -102,7 +102,7 @@ If your language is not listed in Crowdin, please open an issue.
During development, it is useful to use the During development, it is useful to use the
``` ```
yarn dev pnpm dev
``` ```
command. command.
@@ -112,7 +112,7 @@ command.
- Service Worker is watched by esbuild. - Service Worker is watched by esbuild.
## Testing ## Testing
- Test codes are located in [`/test`](/test). - Test codes are located in [`/packages/backend/test`](/packages/backend/test).
### Run test ### Run test
Create a config file. Create a config file.
@@ -127,12 +127,12 @@ Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.y
Run all test. Run all test.
``` ```
yarn test pnpm test
``` ```
#### Run specify test #### Run specify test
``` ```
yarn jest -- foo.ts pnpm jest -- foo.ts
``` ```
### e2e tests ### e2e tests
@@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。 これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Notes ## Notes
### How to resolve conflictions occurred at yarn.lock? ### How to resolve conflictions occurred at pnpm-lock.yaml?
Just execute `yarn` to fix it. Just execute `pnpm` to fix it.
### INSERTするときにはsaveではなくinsertを使用する ### INSERTするときにはsaveではなくinsertを使用する
#6441 #6441
@@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
### Migration作成方法 ### Migration作成方法
packages/backendで: packages/backendで:
```sh ```sh
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name> pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
``` ```
- 生成後、ファイルをmigration下に移してください - 生成後、ファイルをmigration下に移してください
+13 -6
View File
@@ -2,8 +2,12 @@ ARG NODE_VERSION=18.13.0-bullseye
FROM node:${NODE_VERSION} AS builder FROM node:${NODE_VERSION} AS builder
RUN apt-get update \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
&& apt-get install -y --no-install-recommends \ --mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -yqq --no-install-recommends \
build-essential build-essential
RUN corepack enable RUN corepack enable
@@ -16,7 +20,8 @@ COPY ["packages/backend/package.json", "./packages/backend/"]
COPY ["packages/frontend/package.json", "./packages/frontend/"] COPY ["packages/frontend/package.json", "./packages/frontend/"]
COPY ["packages/sw/package.json", "./packages/sw/"] COPY ["packages/sw/package.json", "./packages/sw/"]
RUN pnpm i --frozen-lockfile RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output
COPY . ./ COPY . ./
@@ -30,11 +35,13 @@ FROM node:${NODE_VERSION}-slim AS runner
ARG UID="991" ARG UID="991"
ARG GID="991" ARG GID="991"
RUN apt-get update \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ffmpeg tini \ ffmpeg tini \
&& apt-get -y clean \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable \ && corepack enable \
&& groupadd -g "${GID}" misskey \ && groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
+2
View File
@@ -24,6 +24,8 @@
--- ---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div> </div>
<div> <div>
-5
View File
@@ -133,11 +133,6 @@ id: "aid"
# IP address family used for outgoing request (ipv4, ipv6 or dual) # IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4 #outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128
+2 -2
View File
@@ -68,7 +68,7 @@ export: "Export"
files: "Dateien" files: "Dateien"
download: "Herunterladen" download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden." driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden."
unfollowConfirm: "Möchtest du {name} nicht mehr folgen?" unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen" lists: "Listen"
@@ -94,7 +94,7 @@ defaultNoteVisibility: "Standardsichtbarkeit"
follow: "Folgen" follow: "Folgen"
followRequest: "Follow-Anfrage senden" followRequest: "Follow-Anfrage senden"
followRequests: "Follow-Anfragen" followRequests: "Follow-Anfragen"
unfollow: "Nicht mehr folgen" unfollow: "Entfolgen"
followRequestPending: "Follow-Anfrage ausstehend" followRequestPending: "Follow-Anfrage ausstehend"
enterEmoji: "Gib ein Emoji ein" enterEmoji: "Gib ein Emoji ein"
renote: "Renote" renote: "Renote"
+1 -1
View File
@@ -68,7 +68,7 @@ export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure that you want to unfollow {name}?" unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while." importRequested: "You've requested an import. This may take a while."
lists: "Lists" lists: "Lists"
+321 -3
View File
@@ -509,7 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor" serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos" deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
newNoteRecived: "Tienes una nota nuevo" newNoteRecived: "Tienes una nota nueva"
sounds: "Sonidos" sounds: "Sonidos"
sound: "Sonidos" sound: "Sonidos"
listen: "Escuchar" listen: "Escuchar"
@@ -918,17 +918,323 @@ tools: "Utilidades"
cannotLoad: "No se puede cargar." cannotLoad: "No se puede cargar."
numberOfProfileView: "Número de vistas de perfil" numberOfProfileView: "Número de vistas de perfil"
like: "¡Muy bien!" like: "¡Muy bien!"
unlike: "Quitar 'me gusta'"
numberOfLikes: "Cantidad de 'Me gusta'"
show: "Apariencia" show: "Apariencia"
neverShow: "No mostrar de nuevo"
remindMeLater: "Recordar después"
didYouLikeMisskey: "¿Te gusta Misskey?"
pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!"
roles: "Roles"
role: "Roles"
normalUser: "Usuario normal"
undefined: "Indefinido"
assign: "Asignar"
unassign: "Quitar"
color: "Color" color: "Color"
manageCustomEmojis: "Administrar emojis personalizados"
youCannotCreateAnymore: "Se alcanzó el límite de creación"
cannotPerformTemporary: "Indisponible temporalmente"
cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo."
preset: "Predefinido"
selectFromPresets: "Escoger desde predefinidos"
achievements: "Logros"
_achievements:
earnedAt: "Desbloqueado el"
_types:
_notes1:
title: "¡Hola Misskey!"
description: "Publicaste tu primera nota"
flavor: "¡Pasándola bien con Misskey!"
_notes10:
title: "Algunas notas"
description: "10 notas publicadas"
_notes100:
title: "¡Muchas notas!"
description: "100 notas publicadas"
_notes500:
title: "¡Cubierto de notas!"
description: "500 notas publicadas"
_notes1000:
title: "¡Una montaña de notas!"
description: "1000 notas publicadas"
_notes5000:
title: "¡Exceso de notas!"
description: "5000 notas publicadas"
_notes10000:
title: "¡Súpernota!"
description: "10000 notas publicadas"
_notes20000:
title: "Necesito... Más... ¡Notas!"
description: "20000 notas publicadas"
_notes30000:
title: "¡Notas! ¡Notas! ¡Notas!"
description: "30000 notas publicadas"
_notes40000:
title: "Fábrica de notas"
description: "40000 notas publicadas"
_notes50000:
title: "¡Un planeta de notas!"
description: "50000 notas publicadas"
_notes60000:
title: "¡Un cuásar de notas!"
description: "60000 notas publicadas"
_notes70000:
title: "¡Un hoyo negro de notas!"
description: "70000 notas publicadas"
_notes80000:
title: "¡Una galaxia de notas!"
description: "80000 notas publicadas"
_notes90000:
title: "¡Todo un universo de notas!"
description: "90000 notas publicadas"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "100000 notas publicadas"
flavor: "¿Tienes tanto para publicar?"
_login3:
title: "Principiante I"
description: "Días desde el inicio de sesión: 3"
flavor: "Desde hoy, soy Misskero"
_login7:
title: "Principiante II"
description: "Días desde el inicio de sesión: 7"
flavor: "¿Ya te acostumbraste?"
_login15:
title: "Principiante III"
description: "Días desde el inicio de sesión: 15"
_login30:
title: "Misskero I"
description: "Días desde el inicio de sesión: 30"
_login60:
title: "Misskero II"
description: "Días desde el inicio de sesión: 60"
_login100:
title: "Misskero III"
description: "Días desde el inicio de sesión: 100"
flavor: "Para este usuario, Misskaína"
_login200:
title: "Regular I"
description: "Días desde el inicio de sesión: 200"
_login300:
title: "Regular II"
description: "Días desde el inicio de sesión: 300"
_login400:
title: "Regular III"
description: "Días desde el inicio de sesión: 400"
_login500:
title: "Veterano I"
description: "Días desde el inicio de sesión: 500"
flavor: "Chicos, me encantan las libretas..."
_login600:
title: "Veterano II"
description: "Días desde el inicio de sesión: 600"
_login700:
title: "Veterano III"
description: "Días desde el inicio de sesión: 700"
_login800:
title: "Maestro I"
description: "Días desde el inicio de sesión: 800"
_login900:
title: "Maestro II"
description: "Días desde el inicio de sesión: 900"
_login1000:
title: "Maestro III"
description: "Días desde el inicio de sesión: 1000"
flavor: "¡Gracias por usar Misskey!"
_noteClipped1:
title: "No puedo evitar clipearte..."
description: "Hacer un clip por primera vez"
_noteFavorited1:
title: "Contemplando las estrellas"
description: "Poner una nota como favorito por primera vez"
_myNoteFavorited1:
title: "¡Quiero una estrella!"
description: "Tu nota ha sido marcada como favorito por primera vez"
_profileFilled:
title: "¡Listo!"
description: "Perfil completado"
_markedAsCat:
title: "Soy un gato"
description: "Configurar la cuenta como cuenta de un gato"
flavor: "Aún no tengo nombre"
_following1:
title: "Primera vez siguiendo a alguien"
description: "Seguir a un usuario"
_following10:
title: "Ahí la llevas, ahí la llevas..."
description: "10 usuarios seguidos"
_following50:
title: "¡Un puñado de amigos!"
description: "50 cuentas seguidas"
_following100:
title: "100 amigos"
description: "100 cuentas seguidas"
_following300:
title: "¡Sobrecarga de amigos!"
description: "300 cuentas seguidas"
_followers1:
title: "¡Tu primer seguidor!"
description: "1 seguidor ganado"
_followers10:
title: "¡Sígueme!"
description: "10 seguidores ganados"
_followers50:
title: "Viniendo en manada"
description: "50 seguidores ganados"
_followers100:
title: "Popular"
description: "100 cuentas seguidas"
_followers300:
title: "Por favor, hagan una fila"
description: "300 seguidores ganados"
_followers500:
title: "¡Toda una torre de radio!"
description: "500 seguidores ganados"
_followers1000:
title: "\"Influyente\""
description: "1000 seguidores gandos"
_collectAchievements30:
title: "Coleccionista"
description: "30 logros ganados"
_viewAchievements3min:
title: "¡Te gustan los logros!"
description: "Mirando tus logros por 3 minutos"
_iLoveMisskey:
title: "¡AMO Misskey!"
description: "\"I ❤ #Misskey\" Publicado"
flavor: "El equipo de desarrollo de Misskey, en verdad, ¡aprecia tu apoyo!"
_foundTreasure:
title: "Búsqueda del tesoro"
description: "Encontraste un tesoro"
_client30min:
title: "Un descansito"
description: "30 minutos dedicados a Misskey"
_noteDeletedWithin1min:
title: "Ah... Mejor no..."
description: "Borrar una nota antes que de pase 1 minuto"
_postedAtLateNight:
title: "Nocturno"
description: "Una nota publicada por la noche"
flavor: "¡Ya casi es hora de dormir!"
_postedAt0min0sec:
title: "Reloj parlante"
description: "Publicar una nota a las 00:00 de la madrugada"
flavor: "Tic, tic, tic ¡TUUUUUN!"
_selfQuote:
title: "Autoreferencia"
description: "Citar tu propia nota"
_htl20npm:
title: "Línea de tiempo fluyendo"
description: "La velocidad de tu línea de tiempo excede las 20 npm (notas por minuto)"
_viewInstanceChart:
title: "Analista"
description: "Gráficas de la instancia mostradas"
_outputHelloWorldOnScratchpad:
title: "¡Hola mundo!"
description: "Escribir \"hello world\" en el compositor"
_open3windows:
title: "Multiventana"
description: "Tener más de 3 ventanas al mismo tiempo"
_driveFolderCircularReference:
title: "Referencia circular"
description: "Intento de crear carpetas recursivamente"
_reactWithoutRead:
title: "¡Sí lo leíste bien?"
description: "Reaccionar a los 3 segundos de publicación de una nota con más de 100 caracteres"
_clickedClickHere:
title: "Pícale aquí"
description: "Le picó ahí"
_justPlainLucky:
title: "Pura suerte"
description: "Obtenido con una probabilidad del 0.01% cada 10 segundos"
_setNameToSyuilo:
title: "Complejo de superioridad"
description: "Configurar el nombre como 'Syuilo'"
_passedSinceAccountCreated1:
title: "Primer aniversario"
description: "Pasó un año desde la creación de la cuenta"
_passedSinceAccountCreated2:
title: "Segundo aniversario"
description: "Pasaron dos años desde la creación de la cuenta"
_passedSinceAccountCreated3:
title: "Tercer aniversario"
description: "Pasaron tres años desde la creación de la cuenta"
_loggedInOnBirthday:
title: "¡Feliz cumpleaños!"
description: "En linea el día de tu cumpleaños"
_loggedInOnNewYearsDay:
title: "¡Feliz Año Nuevo!"
description: "En linea en año nuevo"
flavor: "¡Gracias por tu apoyo a la instancia durante todo este año!"
_cookieClicked:
title: "Un juego para picarle a una galleta"
description: "Picaste una galleta"
flavor: "¿Está mal este juego?"
_brainDiver:
title: "Brain Diver"
description: "Publicaste un vínculo a \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "Crear rol"
edit: "Editar rol"
name: "Nombre del rol"
description: "Descripción del rol"
permission: "Permisos del rol"
descriptionOfPermission: "<b>Moderador</b> Te permite ejecutar acciones básicas de moderación.\n<b>Administradores</b> puede cambiar todas las configuraciones de la instancia."
assignTarget: "Asignar objetivo"
descriptionOfAssignTarget: "<b>Manual</b> Para cambiar manualmente lo que se incluye en este rol.\n<b>Condicional</b> configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente."
manual: "manual"
conditional: "condicional"
condition: "condición"
isConditionalRole: "Esto es un rol condicional"
isPublic: "Publicar rol"
descriptionOfIsPublic: "Cualquiera puede ver los usuarios asignados a este rol. También, el perfil del usuario mostrará este rol."
options: "Opción"
policies: "Política"
baseRole: "Rol base"
useBaseValue: "Usar los valores del rol base"
chooseRoleToAssign: "Selecciona el rol para asignar"
canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad" priority: "Prioridad"
_priority: _priority:
low: "Baja" low: "Baja"
middle: "Mediano" middle: "Mediano"
high: "Alta" high: "Alta"
_options:
gtlAvailable: "Explorar la línea de tiempo global"
ltlAvailable: "Explorar la línea de tiempo local"
canPublicNote: "Permitir la publicación"
canInvite: "Puede crear códigos de invitación"
canManageCustomEmojis: "Administrar emojis personalizados"
driveCapacity: "Capacidad de almacenamiento"
pinMax: "Máximo de notas fijadas"
antennaMax: "Máximo de antenas"
wordMuteMax: "Máximo de caracteres en palabras silenciadas"
webhookMax: "Máximo de Webhooks"
clipMax: "Máximo de clips"
noteEachClipsMax: "Máximo de notas con clip"
userListMax: "Máximo de listas de usuarios"
userEachUserListsMax: "Máximo de usuarios en una lista"
rateLimitFactor: "Limitador"
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
canHideAds: "Puede ocultar anuncios"
_condition:
isLocal: "Usuario local"
isRemote: "Usuario remoto"
createdLessThan: "Menos de X han pasado desde la creación de la cuenta"
createdMoreThan: "Más de X han pasado desde la creación de la cuenta"
followersLessThanOrEq: "Tiene X o menos seguidores"
followersMoreThanOrEq: "Tiene X o más seguidores"
followingLessThanOrEq: "Sigue X o menos cuentas"
followingMoreThanOrEq: "Sigue X o más cuentas"
and: "Condicional AND"
or: "Condicional OR"
not: "Condicional NOT"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor." description: "Reduce el esfuerzo de la moderación en el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
sensitivity: "Sensibilidad de detección" sensitivity: "Sensibilidad de la detección"
sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)." sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)."
setSensitiveFlagAutomatically: "Marcar como NSFW" setSensitiveFlagAutomatically: "Marcar como NSFW"
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada." setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada."
@@ -1328,10 +1634,12 @@ _widgets:
jobQueue: "Cola de trabajos" jobQueue: "Cola de trabajos"
serverMetric: "Estadísticas del servidor" serverMetric: "Estadísticas del servidor"
aiscript: "Consola de AiScript" aiscript: "Consola de AiScript"
aiscriptApp: "Aplicación AiScript"
aichan: "indigo" aichan: "indigo"
userList: "Lista de usuarios" userList: "Lista de usuarios"
_userList: _userList:
chooseList: "Seleccione una lista" chooseList: "Seleccione una lista"
clicker: "Cliqueador"
_cw: _cw:
hide: "Ocultar" hide: "Ocultar"
show: "Ver más" show: "Ver más"
@@ -1434,7 +1742,16 @@ _timelines:
social: "Social" social: "Social"
global: "Global" global: "Global"
_play: _play:
new: "Crear guión"
edit: "Editar guión"
created: "Guión creado"
updated: "Guión editado"
deleted: "Guión eliminado"
pageSetting: "Configuración de guión"
editThisPage: "Editar este guión"
viewSource: "Ver la fuente" viewSource: "Ver la fuente"
my: "Mis guiones"
liked: "Guiones que te gustaron"
featured: "Popular" featured: "Popular"
title: "Título" title: "Título"
script: "Script" script: "Script"
@@ -1507,6 +1824,7 @@ _notification:
pollEnded: "Estan disponibles los resultados de la encuesta" pollEnded: "Estan disponibles los resultados de la encuesta"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Se han actualizado las notificaciones push" emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
_types: _types:
all: "Todo" all: "Todo"
follow: "Siguiendo" follow: "Siguiendo"
+200
View File
@@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..."
ok: "OK" ok: "OK"
gotIt: "Saya mengerti" gotIt: "Saya mengerti"
cancel: "Batalkan" cancel: "Batalkan"
noThankYou: "Tidak sekarang."
enterUsername: "Masukkan nama pengguna" enterUsername: "Masukkan nama pengguna"
renotedBy: "direnote oleh {user}" renotedBy: "direnote oleh {user}"
noNotes: "Tidak ada catatan" noNotes: "Tidak ada catatan"
@@ -206,6 +207,7 @@ done: "Selesai"
processing: "Memproses" processing: "Memproses"
preview: "Pratinjau" preview: "Pratinjau"
default: "Bawaan" default: "Bawaan"
defaultValueIs: "Bawaan: {value}"
noCustomEmojis: "Tidak ada emoji kustom" noCustomEmojis: "Tidak ada emoji kustom"
noJobs: "Tidak ada kerja" noJobs: "Tidak ada kerja"
federating: "memfederasi" federating: "memfederasi"
@@ -349,6 +351,8 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Nyalakan reCAPTCHA" enableRecaptcha: "Nyalakan reCAPTCHA"
recaptchaSiteKey: "Site key" recaptchaSiteKey: "Site key"
recaptchaSecretKey: "Secret Key" recaptchaSecretKey: "Secret Key"
turnstile: "Turnstile"
enableTurnstile: "Nyalakan Turnstile"
turnstileSiteKey: "Site key" turnstileSiteKey: "Site key"
turnstileSecretKey: "Secret Key" turnstileSecretKey: "Secret Key"
avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal." avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal."
@@ -454,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna"
groupInvited: "Telah diundang ke grup" groupInvited: "Telah diundang ke grup"
aboutX: "Tentang {x}" aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji" emojiStyle: "Gaya emoji"
native: "Native"
disableDrawer: "Jangan gunakan menu bergaya laci" disableDrawer: "Jangan gunakan menu bergaya laci"
youHaveNoGroups: "Kamu tidak memiliki grup" youHaveNoGroups: "Kamu tidak memiliki grup"
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
@@ -857,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar" cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?" cropImageAsk: "Ingin memotong gambar?"
file: "Berkas" file: "Berkas"
recentNHours: "{n} jam terakhir"
recentNDays: "{n} hari terakhir"
noEmailServerWarning: "Mail Server tidak disetel." noEmailServerWarning: "Mail Server tidak disetel."
thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan."
recommended: "Disarankan" recommended: "Disarankan"
check: "Cek" check: "Cek"
driveCapOverrideLabel: "Ubah kapasitas drive untuk user ini"
driveCapOverrideCaption: "Setel ulang kapasitas ke bawaan dengan memasukkan nilai 0 atau lebih rendah."
requireAdminForView: "Kamu harus login dengan akun administrator untuk melihat ini."
isSystemAccount: "Akun yang dibuat dan otomatis dioperasikan oleh sistem."
typeToConfirm: "Mohon masukkan {x} untuk mengonfirmasi"
deleteAccount: "Hapus Akun" deleteAccount: "Hapus Akun"
document: "Dokumen"
numberOfPageCache: "Jumlah halaman ditembolokkan"
numberOfPageCacheDescription: "Menaikkan jumlah ini akan meningkatkan kenyamanan untuk pengguna, namun dapat menyebabkan lonjakan beban pada peladen dan juga memori yang digunakan."
logoutConfirm: "Anda yakin ingin keluar?" logoutConfirm: "Anda yakin ingin keluar?"
lastActiveDate: "Terakhir digunakan" lastActiveDate: "Terakhir digunakan"
statusbar: "Bilah status" statusbar: "Bilah status"
@@ -870,20 +886,189 @@ colored: "Diwarnai"
refreshInterval: "Jeda pembaharuan" refreshInterval: "Jeda pembaharuan"
label: "Label" label: "Label"
type: "Tipe" type: "Tipe"
speed: "Kecepatan"
slow: "Lambat"
fast: "Cepat"
sensitiveMediaDetection: "Deteksi media NSFW"
localOnly: "Hanya lokal" localOnly: "Hanya lokal"
remoteOnly: "Hanya remot"
failedToUpload: "Gagal mengunggah"
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
beta: "Beta"
enableAutoSensitive: "Penandaan NSFW otomatis"
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi."
navbar: "Bilah navigasi"
shuffle: "Acak" shuffle: "Acak"
account: "Akun" account: "Akun"
move: "Pindah"
pushNotification: "Pemberitahuan push"
subscribePushNotification: "Nyalakan pemberitahuan push"
unsubscribePushNotification: "Matikan pemberitahuan push"
pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan"
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push"
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
windowMaximize: "Maksimalkan"
windowRestore: "Kembalikan"
caption: "Keterangan"
loggedInAsBot: "Sedang login sebagai bot"
tools: "Alat"
cannotLoad: "Tidak dapat memuat"
numberOfProfileView: "tayang profil"
like: "Suka" like: "Suka"
unlike: "Tidak Suka" unlike: "Tidak Suka"
numberOfLikes: "Jumlah yang disukai" numberOfLikes: "Jumlah yang disukai"
show: "Tampilkan" show: "Tampilkan"
neverShow: "Jangan tampilkan lagi"
remindMeLater: "Mungkin nanti"
didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?"
pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!"
roles: "Peran"
role: "Peran"
color: "Warna" color: "Warna"
_achievements:
_types:
_login7:
description: "Login selama 7 hari"
flavor: "Sudah mulai terbiasa?"
_login15:
title: "Pemula III"
description: "Login selama 15 hari"
_login30:
title: "Misskist I"
description: "Login selama 30 hari"
_login60:
title: "Misskist II"
description: "Login selama 60 hari"
_login100:
title: "Misskist III"
description: "Login selama 100 hari"
flavor: "Violent Misskist"
_login200:
title: "Reguler I"
description: "Login selama 200 hari"
_login300:
title: "Reguler II"
description: "Login selama 300 hari"
_login400:
title: "Reguler III"
description: "Login selama 400 hari"
_login500:
title: "Veteran I"
description: "Login selama 500 hari"
flavor: "Kawanku, aku suka catatan."
_login600:
title: "Veteran II"
description: "Login selama 600 hari"
_login700:
title: "Veteran III"
description: "Login selama 700 hari"
_login800:
title: "Sepuh Catatan I"
description: "Login selama 800 hari"
_login900:
title: "Sepuh Catatan II"
description: "Login selama 900 hari"
_login1000:
title: "Sepuh Catatan III"
description: "Login selama 1000 hari"
flavor: "Terima kasih telah menggunakan Misskey!"
_noteClipped1:
title: "Harus... Ngeklip..."
description: "Klip catatan pertamamu"
_noteFavorited1:
title: "Pengamat Bintang"
description: "Favoritkan catatan pertamamu"
_myNoteFavorited1:
title: "Pencari Bintang"
description: "Minta orang lain memfavoritkan salah satu catatanmu"
_profileFilled:
title: "Siap Sedia"
description: "Atur profil kamu"
_markedAsCat:
title: "Aku Seekor Kucing"
description: "Tandai akunmu sebagai kucing"
flavor: "Aku beri kamu nama nanti"
_following1:
title: "Ikuti pengguna lain pertamamu"
description: "Ikuti pengguna"
_following10:
title: "Terusin... terusin..."
description: "Ikuti 10 pengguna lain"
_following50:
title: "Banyak teman"
description: "Ikuti 50 pengguna lain"
_following100:
title: "100 Teman"
description: "Ikuti 100 pengguna lain"
_clickedClickHere:
description: "Kamu telah mengeklik disini"
_justPlainLucky:
title: "Lagi Beruntung"
description: "Mendapatkan kesempatan dengan kemungkinan 0.01% setiap 10 detik"
_setNameToSyuilo:
title: "God Complex"
description: "Atur namamu jadi \"syuilo\""
_passedSinceAccountCreated1:
title: "Perayaan Satu Tahun"
description: "Satu tahun telah lewat sejak akunmu dibuat"
_passedSinceAccountCreated2:
title: "Perayaan Dua Tahun"
description: "Dua tahun telah lewat sejak akunmu dibuat"
_passedSinceAccountCreated3:
title: "Perayaan Tiga Tahun"
description: "Tiga tahun telah lewat sejak akunmu dibuat"
_loggedInOnBirthday:
title: "Selamat Ulang Tahun"
description: "Login di hari ulang tahunmu"
_loggedInOnNewYearsDay:
title: "Selamat Tahun Baru!"
description: "Login di hari pertama tahun baru"
_cookieClicked:
title: "Permainan dimana kamu mengeklik kue"
description: "Mengeklik kue"
flavor: "Tunggu, apakah kamu sedang berada di website yang benar?"
_brainDiver:
title: "Brain Diver"
description: "Posting tautan mengenai Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "Buat peran"
edit: "Sunting peran"
name: "Nama peran"
description: "Deskripsi peran"
permission: "Perijinan peran"
descriptionOfPermission: "<b>Moderator</b> dapat melakukan operasi moderasi dasar.\n<b>Administrator</b> dapat mengubah seluruh pengaturan instansi."
assignTarget: "Tipe tugas"
descriptionOfAssignTarget: "<b>Manual</b> untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\n<b>Kondisional</b> untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan."
manual: "Manual"
conditional: "Kondisional"
condition: "Kondisi"
isConditionalRole: "Ini adalah peran kondisional"
isPublic: "Publikkan Peran"
descriptionOfIsPublic: "Siapapun dapat melihat daftar pengguna yang ditugaskan pada peran ini. Tambahan juga peran ini akan ditampilkan ke dalam profil pengguna tentang peran yang ditugaskan."
options: "Opsi peran"
policies: "Kebijakan"
baseRole: "Templat peran"
useBaseValue: "Gunakan nilai templat peran"
chooseRoleToAssign: "Pilih peran yang ditugaskan"
canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini"
descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini."
priority: "Prioritas" priority: "Prioritas"
_priority: _priority:
low: "Rendah" low: "Rendah"
middle: "Sedang" middle: "Sedang"
high: "Tinggi" high: "Tinggi"
_options:
gtlAvailable: "Dapat melihat linimasa global"
ltlAvailable: "Dapat melihat linimasa lokal"
canPublicNote: "Dapat mengirim catatan publik"
canInvite: "Dapat membuat kode undangan instansi"
canManageCustomEmojis: "Dapat mengelola Emoji kustom"
driveCapacity: "Kapasitas Drive"
pinMax: "Jumlah maksimal catatan yang disematkan"
_emailUnavailable: _emailUnavailable:
used: "Alamat surel ini telah digunakan" used: "Alamat surel ini telah digunakan"
format: "Format tidak valid." format: "Format tidak valid."
@@ -1167,6 +1352,7 @@ _tutorial:
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey." step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey."
step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}." step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}."
step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀" step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
step8_3: "Kamu dapat mengganti pengaturan ini nanti."
_2fa: _2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
registerDevice: "Daftarkan perangkat baru" registerDevice: "Daftarkan perangkat baru"
@@ -1241,10 +1427,13 @@ _widgets:
trends: "Tren" trends: "Tren"
clock: "Jam" clock: "Jam"
rss: "Pembaca RSS" rss: "Pembaca RSS"
rssTicker: "RSS-Ticker"
activity: "Aktivitas" activity: "Aktivitas"
photos: "Foto" photos: "Foto"
digitalClock: "Jam digital" digitalClock: "Jam digital"
unixClock: "Jam UNIX"
federation: "Federasi" federation: "Federasi"
instanceCloud: "Instansi awan"
postForm: "Buat catatan" postForm: "Buat catatan"
slideshow: "Slideshow" slideshow: "Slideshow"
button: "Tombol" button: "Tombol"
@@ -1254,8 +1443,10 @@ _widgets:
aiscript: "Konsol AiScript" aiscript: "Konsol AiScript"
aiscriptApp: "Aplikasi AiScript" aiscriptApp: "Aplikasi AiScript"
aichan: "Ai" aichan: "Ai"
userList: "Daftar pengguna"
_userList: _userList:
chooseList: "Pilih daftar" chooseList: "Pilih daftar"
clicker: "Pengeklik"
_cw: _cw:
hide: "Sembunyikan" hide: "Sembunyikan"
show: "Lihat konten" show: "Lihat konten"
@@ -1319,6 +1510,7 @@ _profile:
changeBanner: "Ubah header" changeBanner: "Ubah header"
_exportOrImport: _exportOrImport:
allNotes: "Semua catatan" allNotes: "Semua catatan"
favoritedNotes: "Catatan favorit"
followingList: "Ikuti" followingList: "Ikuti"
muteList: "Bisukan" muteList: "Bisukan"
blockingList: "Blokir" blockingList: "Blokir"
@@ -1437,7 +1629,9 @@ _notification:
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
youWereInvitedToGroup: "Telah diundang ke grup" youWereInvitedToGroup: "Telah diundang ke grup"
pollEnded: "Hasil Kuesioner telah keluar" pollEnded: "Hasil Kuesioner telah keluar"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Pembaruan notifikasi dorong" emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
achievementEarned: "Pencapaian didapatkan"
_types: _types:
all: "Semua" all: "Semua"
follow: "Ikuti" follow: "Ikuti"
@@ -1459,6 +1653,7 @@ _deck:
alwaysShowMainColumn: "Selalu tampilkan kolom utama" alwaysShowMainColumn: "Selalu tampilkan kolom utama"
columnAlign: "Luruskan kolom" columnAlign: "Luruskan kolom"
addColumn: "Tambahkan kolom" addColumn: "Tambahkan kolom"
configureColumn: "Atur kolom"
swapLeft: "Pindah ke kiri" swapLeft: "Pindah ke kiri"
swapRight: "Pindah ke kanan" swapRight: "Pindah ke kanan"
swapUp: "Pindah ke atas" swapUp: "Pindah ke atas"
@@ -1466,6 +1661,11 @@ _deck:
stackLeft: "Tumpukkan di kolom kiri" stackLeft: "Tumpukkan di kolom kiri"
popRight: "Keluarkan di kanan" popRight: "Keluarkan di kanan"
profile: "Profil" profile: "Profil"
newProfile: "Profil baru"
deleteProfile: "Hapus profil"
introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!"
introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau."
widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit."
_columns: _columns:
main: "Utama" main: "Utama"
widgets: "Widget" widgets: "Widget"
+89 -16
View File
@@ -22,7 +22,7 @@ instance: "Инстанс"
settings: "Настройки" settings: "Настройки"
basicSettings: "Основные настройки" basicSettings: "Основные настройки"
otherSettings: "Прочие настройки" otherSettings: "Прочие настройки"
openInWindow: "Открывать в плавающих окнах" openInWindow: "Открыть в плавающем окне"
profile: "Профиль" profile: "Профиль"
timeline: "Лента" timeline: "Лента"
noAccountDescription: "Пользователь ничего не написал про себя" noAccountDescription: "Пользователь ничего не написал про себя"
@@ -273,7 +273,7 @@ light: "Светлый"
dark: "Тёмный" dark: "Тёмный"
lightThemes: "Светлые темы" lightThemes: "Светлые темы"
darkThemes: "Тёмные темы" darkThemes: "Тёмные темы"
syncDeviceDarkMode: "Синхронизировать с темным режимом устройства" syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
drive: "Диск" drive: "Диск"
fileName: "Имя файла" fileName: "Имя файла"
selectFile: "Выберите файл" selectFile: "Выберите файл"
@@ -456,6 +456,7 @@ uiLanguage: "Язык интерфейса"
groupInvited: "Приглашение в группу" groupInvited: "Приглашение в группу"
aboutX: "Описание {x}" aboutX: "Описание {x}"
emojiStyle: "Стиль эмодзи" emojiStyle: "Стиль эмодзи"
native: "Системные"
disableDrawer: "Не использовать выдвижные меню" disableDrawer: "Не использовать выдвижные меню"
youHaveNoGroups: "У вас нет ни одной группы" youHaveNoGroups: "У вас нет ни одной группы"
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
@@ -603,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS."
testEmail: "Проверка доставки электронной почты" testEmail: "Проверка доставки электронной почты"
wordMute: "Скрытие слов" wordMute: "Скрытие слов"
regexpError: "Ошибка в регулярном выражении" regexpError: "Ошибка в регулярном выражении"
regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
instanceMute: "Глушение инстансов" instanceMute: "Глушение инстансов"
userSaysSomething: "{name} что-то сообщает" userSaysSomething: "{name} что-то сообщает"
makeActive: "Активировать" makeActive: "Активировать"
@@ -804,7 +806,7 @@ translate: "Перевод"
translatedFrom: "Перевод. Язык оригинала — {x}" translatedFrom: "Перевод. Язык оригинала — {x}"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "ИИ режим" aiChanMode: "Режим Ай"
keepCw: "Сохраняйте Предупреждения о содержимом" keepCw: "Сохраняйте Предупреждения о содержимом"
pubSub: "Учётные записи Pub/Sub" pubSub: "Учётные записи Pub/Sub"
lastCommunication: "Последнее сообщение" lastCommunication: "Последнее сообщение"
@@ -821,8 +823,8 @@ manageAccounts: "Управление аккаунтом"
makeReactionsPublic: "Опубликовать список реакций" makeReactionsPublic: "Опубликовать список реакций"
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим." makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
classic: "Классика" classic: "Классика"
muteThread: "Заглушить цепочку" muteThread: "Скрыть цепочку"
unmuteThread: "Отменить глушение цепочки" unmuteThread: "Отменить сокрытие цепочки"
ffVisibility: "Видимость подписок и подписчиков" ffVisibility: "Видимость подписок и подписчиков"
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков." ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
continueThread: "Показать следующие ответы" continueThread: "Показать следующие ответы"
@@ -891,6 +893,7 @@ cannotUploadBecauseNoFreeSpace: "Файл не может быть загруж
beta: "Бета" beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW" enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта." enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
navbar: "Панель навигации" navbar: "Панель навигации"
shuffle: "Перемешать" shuffle: "Перемешать"
account: "Учётные записи" account: "Учётные записи"
@@ -1096,6 +1099,9 @@ _achievements:
title: "Я люблю Misskey" title: "Я люблю Misskey"
description: "Написана заметка «I ❤ #Misskey»" description: "Написана заметка «I ❤ #Misskey»"
flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков" flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков"
_foundTreasure:
title: "Охота за сокровищами"
description: "Найдено спрятанное сокровище"
_client30min: _client30min:
title: "Перерыв на обед" title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента" description: "Прошло 30 минут с момента запуска клиента"
@@ -1116,6 +1122,9 @@ _achievements:
_htl20npm: _htl20npm:
title: "В потоке" title: "В потоке"
description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)" description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)"
_viewInstanceChart:
title: "Аналитик"
description: "Просмотрены статистические диаграммы инстанса"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Привет, мир!" title: "Привет, мир!"
description: "Выведен текст «hello world» в Когтеточке" description: "Выведен текст «hello world» в Когтеточке"
@@ -1189,7 +1198,34 @@ _role:
middle: "Средне" middle: "Средне"
high: "Высокий" high: "Высокий"
_options: _options:
gtlAvailable: "Может просматривать глобальную ленту"
ltlAvailable: "Может просматривать местную ленту"
canPublicNote: "Может публиковать общедоступные заметки"
canInvite: "Может создавать пригласительные коды"
canManageCustomEmojis: "Управлять пользовательскими эмодзи" canManageCustomEmojis: "Управлять пользовательскими эмодзи"
driveCapacity: "Доступное пространство на «диске»"
pinMax: "Доступное количество закреплённых заметок"
antennaMax: "Доступное количество антенн"
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
clipMax: "Максимальное количество подборок"
noteEachClipsMax: "Максимальное количество заметок в подборке"
userListMax: "Максимальное количество списков аккаунтов"
userEachUserListsMax: "Максимальное количество аккаунтов в списке"
rateLimitFactor: "Ограничение активности"
descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные"
canHideAds: "Может скрыть рекламу"
_condition:
isLocal: "Местный"
isRemote: "Неместный"
createdLessThan: "Аккаунт младше, чем..."
createdMoreThan: "Аккаунт старше, чем..."
followersLessThanOrEq: "Количество подписчиков не превышает…"
followersMoreThanOrEq: "Количество подписчиков не меньше чем…"
followingLessThanOrEq: "Количество подписок не превышает…"
followingMoreThanOrEq: "Количество подписок не меньше чем…"
and: "Выполнено несколько условий:.."
or: "Выполнено любое из условий:.."
not: "Кроме тех, у кого…"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW" setSensitiveFlagAutomatically: "Установить флаг NSFW"
@@ -1237,10 +1273,23 @@ _plugin:
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями" manage: "Управление расширениями"
_preferencesBackups: _preferencesBackups:
saveConfirm: "Сохранить бэкап как {name}?" list: "Существующие резервные копии"
deleteConfirm: "Удалить резервную копию {name}?" saveNew: "Создать резервную копию"
renameConfirm: ереименовать резервную копию с \"{old}\" на \"{new}\"?" loadFile: рочесть из файла"
noBackups: "Резервной копии не существует. Вы можете создать резервную копию в настройках на этом инстансе с помощью \"Создать новую резервную копию\"." apply: "Восстановить на это устройство"
save: "Обновить из текущих настроек"
inputName: "Введите название для резервной копии"
cannotSave: "Сохранить не удалось"
nameAlreadyExists: "Резервная копия под названием «{name}» уже существует. Придумайте другое."
applyConfirm: "Правда хотите загрузить резервную копию «{name}» на это устройство? Этим будут потеряны текущие настройки."
saveConfirm: "Сохранить резервную копию под названием «{name}»?"
deleteConfirm: "Удалить резервную копию «{name}»?"
renameConfirm: "Переименовать резервную копию «{old}» в «{new}»?"
noBackups: "Здесь ещё нет резервных копий. Вы можете создать резервную копию настроек на этом сайте с помощью кнопки «Создать резервную копию»."
createdAt: "Создана {date} в {time}"
updatedAt: "Обновлена {date} в {time}"
cannotLoad: "Загрузить не удалось"
invalidFile: "Некорректный формат файла"
_registry: _registry:
scope: "Область" scope: "Область"
key: "Ключ" key: "Ключ"
@@ -1324,6 +1373,8 @@ _mfm:
sparkleDescription: "Добавляет эффект искрящихся частиц." sparkleDescription: "Добавляет эффект искрящихся частиц."
rotate: "Повернуть" rotate: "Повернуть"
rotateDescription: "Поворачивает на заданный угол." rotateDescription: "Поворачивает на заданный угол."
plain: "Буквально"
plainDescription: "MFM внутри отключается, и текст отображается как есть"
_instanceTicker: _instanceTicker:
none: "Не показывать" none: "Не показывать"
remote: "Только для других сайтов" remote: "Только для других сайтов"
@@ -1353,12 +1404,14 @@ _wordMute:
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты." softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся." hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
soft: "Мягкий" soft: "Мягко"
hard: "Жёсткий" hard: "Жёстко"
mutedNotes: "Скрытые заметки" mutedNotes: "Скрытые заметки"
_instanceMute: _instanceMute:
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
title: "Скрывает заметки с заданных инстансов." title: "Скрывает заметки с заданных инстансов."
heading: "Список заглушенных инстансов" heading: "Список скрытых инстансов"
_theme: _theme:
explore: "Обзор" explore: "Обзор"
install: "Установить тему" install: "Установить тему"
@@ -1479,12 +1532,16 @@ _tutorial:
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!" step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»." step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
step7_3: "Приятно вам провести время с Misskey🚀" step7_3: "Приятно вам провести время с Misskey🚀"
step8_1: "Ах, да, не хотите ли включить push-уведомления?"
step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey."
step8_3: "Эту настройку вы всегда сможете поменять"
_2fa: _2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена." alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerDevice: "Зарегистрируйте ваше устройство" registerDevice: "Зарегистрируйте ваше устройство"
registerKey: "Зарегистрировать ключ" registerKey: "Зарегистрировать ключ"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
step3: "И наконец, введите код, который покажет приложение." step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
@@ -1501,7 +1558,7 @@ _permissions:
"write:following": "Изменять спискок подписок" "write:following": "Изменять спискок подписок"
"read:messaging": "Смотреть сообщения" "read:messaging": "Смотреть сообщения"
"write:messaging": "Писать и удалять сообщения" "write:messaging": "Писать и удалять сообщения"
"read:mutes": "Смотреть спискок скрытых пользователей" "read:mutes": "Смотреть список скрытых пользователей"
"write:mutes": "Изменять список скрытых пользователей" "write:mutes": "Изменять список скрытых пользователей"
"write:notes": "Писать и удалять заметки" "write:notes": "Писать и удалять заметки"
"read:notifications": "Смотреть уведомления" "read:notifications": "Смотреть уведомления"
@@ -1552,10 +1609,13 @@ _widgets:
trends: "Актуальное" trends: "Актуальное"
clock: "Часы" clock: "Часы"
rss: "Просмотр RSS" rss: "Просмотр RSS"
rssTicker: "Бегущая строка RSS"
activity: "Активность" activity: "Активность"
photos: "Фото" photos: "Фото"
digitalClock: "Цифровые часы" digitalClock: "Цифровые часы"
unixClock: "Часы UNIX"
federation: "Федерация" federation: "Федерация"
instanceCloud: "Облако инстансов"
postForm: "Форма отправки" postForm: "Форма отправки"
slideshow: "Показ слайдов" slideshow: "Показ слайдов"
button: "Кнопка" button: "Кнопка"
@@ -1563,9 +1623,12 @@ _widgets:
jobQueue: "Очередь заданий" jobQueue: "Очередь заданий"
serverMetric: "Показатели сервера" serverMetric: "Показатели сервера"
aiscript: "Консоль AiScript" aiscript: "Консоль AiScript"
aiscriptApp: "Приложение на AiScript"
aichan: "Ай" aichan: "Ай"
userList: "Список аккаунтов"
_userList: _userList:
chooseList: "Выберите список" chooseList: "Выберите список"
clicker: "Счётчик щелчков"
_cw: _cw:
hide: "Спрятать" hide: "Спрятать"
show: "Показать еще" show: "Показать еще"
@@ -1628,12 +1691,13 @@ _profile:
changeAvatar: "Поменять аватар" changeAvatar: "Поменять аватар"
changeBanner: "Поменять изображение в шапке" changeBanner: "Поменять изображение в шапке"
_exportOrImport: _exportOrImport:
allNotes: "Все записи\n" allNotes: "Все заметки\n"
favoritedNotes: "Избранное"
followingList: "Подписки" followingList: "Подписки"
muteList: "Скрытые" muteList: "Скрытые"
blockingList: "Заблокированные" blockingList: "Заблокированные"
userLists: "Списки" userLists: "Списки"
excludeMutingUsers: "За исключением заглушенных пользователей" excludeMutingUsers: "За исключением скрытых пользователей"
excludeInactiveUsers: "Без неактивных учётных записей" excludeInactiveUsers: "Без неактивных учётных записей"
_charts: _charts:
federation: "Федерация" federation: "Федерация"
@@ -1737,6 +1801,8 @@ _notification:
youReceivedFollowRequest: "У вас новый запрос на подписку." youReceivedFollowRequest: "У вас новый запрос на подписку."
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен." yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
youWereInvitedToGroup: "Вы приглашены в группу." youWereInvitedToGroup: "Вы приглашены в группу."
pollEnded: "Подведены окончательные итоги опроса"
emptyPushNotificationMessage: "Обновлены push-уведомления"
achievementEarned: "Получено достижение" achievementEarned: "Получено достижение"
_types: _types:
all: "Все" all: "Все"
@@ -1746,11 +1812,13 @@ _notification:
renote: "Репосты" renote: "Репосты"
quote: "Цитаты" quote: "Цитаты"
reaction: "Реакции" reaction: "Реакции"
pollEnded: "Окончания опросов"
receiveFollowRequest: "Получен запрос на подписку" receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен" followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Приглашение в группы" groupInvited: "Приглашение в группы"
app: "Уведомления из приложений" app: "Уведомления из приложений"
_actions: _actions:
followBack: "отвечает взаимной подпиской"
reply: "Ответить" reply: "Ответить"
renote: "Репост" renote: "Репост"
_deck: _deck:
@@ -1764,7 +1832,12 @@ _deck:
swapDown: "Переставить ниже" swapDown: "Переставить ниже"
stackLeft: "В столбик влево" stackLeft: "В столбик влево"
popRight: "Из столбика вправо" popRight: "Из столбика вправо"
profile: "Профиль" profile: "Расстановка"
newProfile: "Новая расстановка"
deleteProfile: "Удаление расстановки"
introduction: "Создайте идеальный интерфейс расставляя колонки как угодно"
introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана."
widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки."
_columns: _columns:
main: "Основная" main: "Основная"
widgets: "Виджеты" widgets: "Виджеты"
+188
View File
@@ -944,48 +944,236 @@ _achievements:
_types: _types:
_notes1: _notes1:
title: "เพียงแค่ตั้งค่า msky ของฉัน" title: "เพียงแค่ตั้งค่า msky ของฉัน"
description: "โพสต์โน้ตครั้งแรกของคุณ"
flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
_notes10:
title: "โน้ตบางอย่าง"
description: "โพสต์ 10 โน้ต"
_notes100:
title: "โน้ตจำนวนมาก"
description: "โพสต์ 100 โน้ต"
_notes500:
title: "ครอบคลุมในโน้ต"
description: "โพสต์ 500 โน้ต"
_notes1000:
title: "ภูเขาแห่งโน้ต"
description: "โพสต์ 1,000 โน้ต"
_notes5000:
title: "โน้ตล้น"
description: "โพสต์ 5,000 โน้ต"
_notes10000:
title: "ซุปเปอร์โน้ต"
description: "โพสต์ 10,000 โน้ต"
_notes20000:
title: "ต้องการ... เพิ่มเติม... โน้ต..."
description: "โพสต์ 20,000 โน้ต"
_notes30000:
title: "โน้ต โน้ต โน้ต!"
description: "โพสต์ 30,000 โน้ต"
_notes40000:
title: "โน้ตโรงงาน"
description: "โพสต์ 40,000 โน้ต"
_notes50000:
title: "ดาวเคราะห์แห่งโน้ต"
description: "โพสต์ 50,000 โน้ต"
_notes60000:
title: "โน้ตควอซาร์"
description: "โพสต์ 60,000 โน้ต"
_notes70000:
title: "โน้ตหลุมดำ"
description: "โพสต์ 70,000 โน้ต"
_notes80000:
title: "โน้ต กาแล็กซี่"
description: "โพสต์ 80,000 โน้ต"
_notes90000:
title: "โน้ต จักรวาล"
description: "โพสต์ 90,000 โน้ต"
_notes100000:
title: "ALL YOUR NOTE ARE BELONG TO US"
description: "โพสต์ 100,000 โน้ต"
flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ"
_login3:
title: "มือใหม่ I"
description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน"
flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์"
_login7:
title: "มือใหม่ II"
description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน"
flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?"
_login15:
title: "มือใหม่ III"
description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
_login30:
title: "มิสคิสท์ I"
description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
_login60:
title: "มิสคิสท์ II"
description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
_login100:
title: "มิสคิสท์ III"
description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
flavor: "ความรุนแรง Misskist"
_login200:
title: "ลูกค้าประจำ I"
description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
_login300:
title: "ลูกค้าประจำ II"
description: "เข้าสู่ระบบเป็นเวลารวม 300 วัน"
_login400:
title: "ลูกค้าประจำ III"
description: "เข้าสู่ระบบเป็นเวลารวม 400 วัน"
_login500:
title: "ผู้เชี่ยวชาญ I"
description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน"
flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต"
_login600:
title: "ผู้เชี่ยวชาญ II"
description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน"
_login700:
title: "ผู้เชี่ยวชาญ III"
description: "เข้าสู่ระบบเป็นเวลารวม 700 วัน"
_login800:
title: "ปรมาจารย์ด้านโน้ต I"
description: "เข้าสู่ระบบเป็นเวลารวม 800 วัน"
_login900:
title: "ปรมาจารย์ด้านโน้ต II"
description: "เข้าสู่ระบบเป็นเวลารวม 900 วัน"
_login1000:
title: "ปรมาจารย์ด้านโน้ต III"
description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน"
flavor: "ขอบคุณที่ใช้ Misskey นะ !"
_noteClipped1:
title: "จะต้อง... คลิป..."
description: "คลิปโน้ตตัวแรกของคุณ"
_noteFavorited1:
title: "สตาร์เกเซอร์"
description: "ชื่นชอบโน้ตแรกของคุณ"
_myNoteFavorited1:
title: "แสวงหาดวงดาว"
description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ"
_profileFilled:
title: "เตรียมไว้อย่างดี"
description: "ตั้งค่าโปรไฟล์ของคุณ"
_markedAsCat:
title: "ฉันเป็นแมว"
description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว"
flavor: "ฉันจะให้ชื่อคุณภายหลังนะ"
_following1:
title: "กำลังติดตามผู้ใช้คนแรกของคุณ"
description: "ติดตามผู้ใช้"
_following10:
title: "ทำต่อไป... ทำต่อไป..."
description: "ติดตาม 10 บัญชีผู้ใช้"
_following50:
title: "มีเพื่อนมากมาย"
description: "ติดตาม 50 บัญชี"
_following100:
title: "เพื่อน 100 คน"
description: "ติดตาม 100 บัญชี"
_following300:
title: "เพื่อนโอเวอร์โหลด"
description: "ติดตาม 300 บัญชี"
_followers1:
title: "ผู้ติดตามคนแรก"
description: "ได้รับ 1 ผู้ติดตาม"
_followers10:
title: "ติดตามฉัน!"
description: "ได้รับ 10 คนผู้ติดตาม"
_followers50:
title: "มากันเป็นฝูง"
description: "ได้รับ 50 ผู้ติดตาม"
_followers100: _followers100:
title: "บุคคลที่เป็นที่นิยม" title: "บุคคลที่เป็นที่นิยม"
description: "ได้รับ 100 ผู้ติดตาม"
_followers300:
title: "กรุณาสร้างบรรทัดเดียวนะคะ"
description: "ได้รับ 300 คนผู้ติดตาม"
_followers500: _followers500:
title: "เสาสัญญาณ" title: "เสาสัญญาณ"
description: "ได้รับ 500 คนผู้ติดตาม"
_followers1000: _followers1000:
title: "ผู้ทรงอิทธิพล" title: "ผู้ทรงอิทธิพล"
description: "ได้รับ 1,000 ผู้ติดตาม"
_collectAchievements30:
title: "นักสะสมความสำเร็จ"
description: "ได้รับความสำเร็จ 30 ครั้ง"
_viewAchievements3min:
title: "ชอบบรรลุผลสําเร็จ"
description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
_iLoveMisskey: _iLoveMisskey:
title: "ฉันรัก Misskey" title: "ฉันรัก Misskey"
description: "โพสต์ \"I ❤ #Misskey\"" description: "โพสต์ \"I ❤ #Misskey\""
flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!"
_foundTreasure: _foundTreasure:
title: "ล่าสมบัติ" title: "ล่าสมบัติ"
description: "คุณพบสมบัติที่ซ่อนอยู่" description: "คุณพบสมบัติที่ซ่อนอยู่"
_client30min: _client30min:
title: "พักผ่อนสักหน่อย" title: "พักผ่อนสักหน่อย"
description: "ใช้เวลา 30 นาทีบน Misskey"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "ไม่เป็นไร" title: "ไม่เป็นไร"
description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์"
_postedAtLateNight: _postedAtLateNight:
title: "กลางคืน" title: "กลางคืน"
description: "โพสต์โน้ตตอนดึกๆ"
flavor: "ได้เวลาเข้านอนแล้วนะ"
_postedAt0min0sec:
title: "นาฬิกาพูดได้"
description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น."
flavor: "คลิก คลิก คลิก แกล๊งๆ"
_selfQuote:
title: "อ้างอิงตนเอง"
description: "อ้างโน้ตย่อของคุณเอง"
_htl20npm:
title: "ไทม์ไลน์ไหล"
description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)"
_viewInstanceChart: _viewInstanceChart:
title: "วิเคราะห์" title: "วิเคราะห์"
description: "ดูแผนภูมิอินสแตนซ์ของคุณ" description: "ดูแผนภูมิอินสแตนซ์ของคุณ"
_outputHelloWorldOnScratchpad:
title: "หวัดดีชาวโลก!"
description: "เอาพุต \"hello world\" ใน Scratchpad"
_open3windows:
title: "มัลติวินโดว์"
description: "มีการเปิดหน้าต่างอย่างน้อย 3 หน้าต่างพร้อมกัน"
_driveFolderCircularReference: _driveFolderCircularReference:
title: "อ้างอิงวงจร" title: "อ้างอิงวงจร"
description: "พยายามสร้างโฟลเดอร์ที่ซ้อนกันแบบวนซ้ำในไดรฟ์"
_reactWithoutRead:
title: "คุณอ่านมันจริงๆหรือเปล่า?"
description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์"
_clickedClickHere: _clickedClickHere:
title: "คลิ๊กที่นี่" title: "คลิ๊กที่นี่"
description: "คุณได้คลิกที่นี่" description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "ครบรอบหนึ่งปี" title: "ครบรอบหนึ่งปี"
description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated2: _passedSinceAccountCreated2:
title: "ครบรอบสองปี" title: "ครบรอบสองปี"
description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated3: _passedSinceAccountCreated3:
title: "ครบรอบสามปี" title: "ครบรอบสามปี"
description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "สุขสันต์วันเกิด" title: "สุขสันต์วันเกิด"
description: "เข้าสู่ระบบในวันเกิดของคุณ" description: "เข้าสู่ระบบในวันเกิดของคุณ"
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "สวัสดีปีใหม่!" title: "สวัสดีปีใหม่!"
description: "เข้าสู่ระบบในวันแรกของปี" description: "เข้าสู่ระบบในวันแรกของปี"
flavor: "อีกปีที่ยอดเยี่ยมในโอกาสนี้เลย"
_cookieClicked: _cookieClicked:
title: "เกมที่คุณคลิกที่คุกกี้"
description: "คลิกคุกกี้" description: "คลิกคุกกี้"
flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?"
_brainDiver: _brainDiver:
title: "Brain Diver" title: "Brain Diver"
description: "โพสต์ลิงก์ไปยัง Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "บทบาทใหม่" new: "บทบาทใหม่"
+70 -4
View File
@@ -529,7 +529,7 @@ state: "Стан"
sort: "Сортування" sort: "Сортування"
ascendingOrder: "За зростанням" ascendingOrder: "За зростанням"
descendingOrder: "За спаданням" descendingOrder: "За спаданням"
scratchpad: "Чернетка" scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey." scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
output: "Вихід" output: "Вихід"
script: "Скрипт" script: "Скрипт"
@@ -688,7 +688,7 @@ pageLikesCount: "Кількість отриманих вподобань сто
pageLikedCount: "Кількість вподобаних сторінок" pageLikedCount: "Кількість вподобаних сторінок"
contact: "Контакт" contact: "Контакт"
useSystemFont: "Використовувати стандартний шрифт системи" useSystemFont: "Використовувати стандартний шрифт системи"
clips: "Добірка" clips: "Добірки"
experimentalFeatures: "Експериментальні функції" experimentalFeatures: "Експериментальні функції"
developer: "Розробник" developer: "Розробник"
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\"" makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
@@ -904,7 +904,7 @@ _achievements:
earnedAt: "Відкрито" earnedAt: "Відкрито"
_types: _types:
_notes1: _notes1:
title: "налаштовую свій msky" title: "Привіт, Misskey!"
description: "Перша нотатка" description: "Перша нотатка"
flavor: "Приємного часу з Misskey!" flavor: "Приємного часу з Misskey!"
_notes10: _notes10:
@@ -1003,9 +1003,19 @@ _achievements:
title: "Майстер нотаток III" title: "Майстер нотаток III"
description: "1000 днів користування загально" description: "1000 днів користування загально"
flavor: "Дякуємо, що користуєтеся Misskey!" flavor: "Дякуємо, що користуєтеся Misskey!"
_noteClipped1:
title: "Не можна не зберегти"
description: "Перша нотатка у добірці"
_noteFavorited1:
title: "Дивитися на зірки"
_myNoteFavorited1: _myNoteFavorited1:
title: "У пошуках зірок" title: "У пошуках зірок"
_profileFilled:
title: "Повна готовність"
description: "Профіль заповнено"
_markedAsCat: _markedAsCat:
title: "Я кіт"
description: "Позначено як акаунт кота"
flavor: "Я дам тобі ім'я пізніше" flavor: "Я дам тобі ім'я пізніше"
_following1: _following1:
title: "Перша підписка" title: "Перша підписка"
@@ -1032,23 +1042,78 @@ _achievements:
title: "Популярна особа" title: "Популярна особа"
description: "Кількість підписників досягла 100" description: "Кількість підписників досягла 100"
_followers300: _followers300:
title: "Ставайте в чергу"
description: "Кількість підписників досягла 300" description: "Кількість підписників досягла 300"
_followers500: _followers500:
title: "Радіовежа"
description: "Кількість підписників досягла 500" description: "Кількість підписників досягла 500"
_followers1000: _followers1000:
title: "Інфлюенсер" title: "Інфлюенсер"
description: "Кількість підписників досягла 1000" description: "Кількість підписників досягла 1000"
_collectAchievements30:
title: "Збирач досягнень"
description: "Отримано 30 досягнень"
_viewAchievements3min:
title: "Шанувальник досягнень"
description: "Переглядати список досягнень принаймні 3 хвилини"
_iLoveMisskey:
title: "I Love Misskey"
description: "Відправлено \"I ❤ #Misskey\""
flavor: "Дякуємо вам, що користуєтесь Misskey! – команда розробників"
_foundTreasure:
title: "Пошуки скарбів"
description: "Ви знайшли прихований скарб"
_client30min:
title: "Коротка перерва"
description: "З моменту запуску клієнта минуло 30 хвилин"
_noteDeletedWithin1min:
title: "Не зважай"
description: "Допис видалено протягом 1 хвилини після публікації"
_postedAtLateNight:
title: "Нічне життя"
description: "Відправити нотатку посеред ночі"
flavor: "Час лягати спати"
_postedAt0min0sec:
title: "Сигнал часу"
description: "Відправити нотатку о 00:00"
_selfQuote:
title: "Самопосилання"
description: "Процитувати власну нотатку"
_htl20npm:
title: "Плинна стрічка"
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
_viewInstanceChart:
title: "Аналітик"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Вивести \"hello world\" у Скретчпаді"
_clickedClickHere:
title: "Натисніть тут"
description: "Натиснуто тут"
_justPlainLucky:
title: "Просто вдача"
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
_setNameToSyuilo:
title: "Комплекс бога"
description: "Встановлено ім'я \"syuilo\""
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "Перша річниця" title: "Перша річниця"
description: "Минув рік з моменту створення акаунта"
_passedSinceAccountCreated2: _passedSinceAccountCreated2:
title: "Друга річниця" title: "Друга річниця"
description: "Минуло 2 роки з моменту створення акаунта"
_passedSinceAccountCreated3: _passedSinceAccountCreated3:
title: "Третя річниця" title: "Третя річниця"
description: "Минуло 3 роки з моменту створення акаунта" description: "Минуло 3 роки з моменту створення акаунта"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "З Днем народження!" title: "З Днем народження!"
description: "Увійти у свій день народження"
_loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року"
_brainDiver: _brainDiver:
title: "Brain Diver" title: "Brain Diver"
description: "Відправити посилання на \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma" flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
priority: "Пріоритет" priority: "Пріоритет"
@@ -1322,7 +1387,7 @@ _tutorial:
step3_1: "Ви успішно налаштували свій обліковий запис?" step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані." step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми." step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!" step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\""
step4_1: "Ви розмістили свій перший запис?" step4_1: "Ви розмістили свій перший запис?"
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій." step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів." step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
@@ -1586,6 +1651,7 @@ _notification:
youReceivedFollowRequest: "Ви отримали запит на підписку" youReceivedFollowRequest: "Ви отримали запит на підписку"
yourFollowRequestAccepted: "Запит на підписку прийнято" yourFollowRequestAccepted: "Запит на підписку прийнято"
youWereInvitedToGroup: "Запрошення до групи" youWereInvitedToGroup: "Запрошення до групи"
achievementEarned: "Досягнення відкрито"
_types: _types:
all: "Все" all: "Все"
follow: "Підписки" follow: "Підписки"
+122 -2
View File
@@ -995,52 +995,163 @@ _achievements:
_login3: _login3:
title: "初学者 I" title: "初学者 I"
description: "连续登录3天" description: "连续登录3天"
flavor: "今天开始我就是Misskist"
_login7: _login7:
title: "初学者 II"
description: "连续登录7天" description: "连续登录7天"
flavor: "您开始习惯了吗?"
_login15: _login15:
title: "初学者 III"
description: "连续登录15天" description: "连续登录15天"
_login30: _login30:
title: "Misskist "
description: "连续登录30天" description: "连续登录30天"
_login60: _login60:
title: "Misskist Ⅱ"
description: "连续登录60天" description: "连续登录60天"
_login100:
title: "Misskist Ⅲ"
description: "总登入100天"
flavor: "那个用户,是Misskist喔"
_login200:
title: "定期联系Ⅰ"
description: "总登录天数200天"
_login300:
title: "定期联系Ⅱ"
description: "总登录天数300天"
_login400:
title: "定期联系Ⅲ"
description: "总登录天数400天"
_login500:
description: "总登录天数500天"
flavor: "诸君,我喜欢贴文"
_login600:
description: "总登录天数600天"
_login700:
description: "总登录天数700天"
_login800:
description: "总登录天数800天"
_login900:
description: "总登录天数900天"
_login1000: _login1000:
description: "总登录天数1000天"
flavor: "感谢您使用Misskey" flavor: "感谢您使用Misskey"
_noteClipped1:
title: "忍不住要收藏到便签"
description: "第一次将贴文贴进便签"
_noteFavorited1: _noteFavorited1:
title: "观星者" title: "观星者"
description: "第一次将帖子加入收藏"
_myNoteFavorited1:
title: "想要星星"
description: "自己的帖子被其他人加入收藏了"
_profileFilled: _profileFilled:
title: "整装待发" title: "整装待发"
description: "设置了个人资料" description: "设置了个人资料"
_markedAsCat: _markedAsCat:
title: "我是猫" title: "我是猫"
description: "将账户设定为一只猫" description: "将账户设定为一只猫"
flavor: "还没有名字"
_following1:
title: "首次关注"
description: "第一次关注别人"
_following10: _following10:
title: "关注,跟随" title: "关注,跟随"
description: "关注超过10人"
_following50: _following50:
title: "我的朋友很多" title: "我的朋友很多"
description: "关注超过50人"
_following100:
title: "我的朋友很多"
description: "关注超过100人"
_following300: _following300:
title: "朋友成群"
description: "关注数超过300" description: "关注数超过300"
_followers1:
title: "最初的关注者"
description: "第一次被关注"
_followers10:
title: "关注我吧!"
description: "拥有超过10名关注者"
_followers50:
title: "三五成群"
description: "拥有超过50名关注者"
_followers100: _followers100:
title: "胜友如云" title: "胜友如云"
description: "拥有超过100名关注者"
_followers300:
title: "排列成行"
description: "拥有超过300名关注者"
_followers500:
title: "信号塔"
description: "拥有超过500名关注者"
_followers1000:
description: "拥有超过1000名关注者"
_collectAchievements30: _collectAchievements30:
title: "成就收藏家"
description: "获得超过30个成就" description: "获得超过30个成就"
_viewAchievements3min: _viewAchievements3min:
title: "成就爱好者"
description: "盯着成就看三分钟" description: "盯着成就看三分钟"
_iLoveMisskey: _iLoveMisskey:
title: "I Love Misskey" title: "I Love Misskey"
description: "发布\"I ❤ #Misskey\"帖子" description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队" flavor: "感谢您使用 Misskey by 开发团队"
_foundTreasure:
title: "寻宝"
description: "发现了隐藏的宝藏"
_client30min:
title: "休息一下!"
description: "启动客户端超过30分钟"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "无话可说"
description: "发帖后一分钟内就将其删除" description: "发帖后一分钟内就将其删除"
_postedAtLateNight: _postedAtLateNight:
title: "夜行者" title: "夜猫子"
description: "深夜发布帖子" description: "深夜发布帖子"
flavor: "差不多该去睡了喔。"
_postedAt0min0sec:
title: "报时"
description: "在0点发布一篇帖子"
flavor: "嘣 嘣 嘣 Biu——!"
_selfQuote:
title: "自我提及"
description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "在AiScript控制台中输出 hello world"
_open3windows:
title: "多窗口"
description: "打开了三个或更多的窗口"
_driveFolderCircularReference:
title: "循环引用"
description: "试图对网盘中的文件夹进行循环嵌套"
_reactWithoutRead:
title: "有好好读过吗?"
description: "在含有100字以上的帖子被发出三秒内做出回应"
_clickedClickHere:
title: "点这里"
description: "点了这里"
_justPlainLucky:
title: "超高校级的幸运"
description: "每10秒有0.01的概率自动获得"
_setNameToSyuilo:
title: "像神一样呐"
description: "将名称设定为syuilo"
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "一周年"
description: "账户创建时间超过1年" description: "账户创建时间超过1年"
_passedSinceAccountCreated2: _passedSinceAccountCreated2:
title: "二周年"
description: "账户创建时间超过2年" description: "账户创建时间超过2年"
_passedSinceAccountCreated3: _passedSinceAccountCreated3:
title: "三周年"
description: "账户创建时间超过3年" description: "账户创建时间超过3年"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "生日快乐" title: "生日快乐"
@@ -1048,6 +1159,15 @@ _achievements:
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "恭贺新禧" title: "恭贺新禧"
description: "在元旦登入" description: "在元旦登入"
flavor: "今年也请对本实例多多指教!"
_cookieClicked:
title: "点击饼干小游戏"
description: "点击了可疑的饼干"
flavor: "是不是软件有问题?"
_brainDiver:
title: "Brain Diver"
description: "发布了包含Brain Diver链接的帖子"
flavor: "Misskey-Misskey La-Tu-Ma"
_role: _role:
new: "创建角色" new: "创建角色"
edit: "编辑角色" edit: "编辑角色"
@@ -1566,7 +1686,7 @@ _profile:
name: "昵称" name: "昵称"
username: "用户名" username: "用户名"
description: "个人简介" description: "个人简介"
youCanIncludeHashtags: "可以包含一个哈希标签。" youCanIncludeHashtags: "可以在个人简介中包含一个#标签。"
metadata: "附加信息" metadata: "附加信息"
metadataEdit: "附加信息编辑" metadataEdit: "附加信息编辑"
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
+21 -21
View File
@@ -240,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪掉「{x}」嗎?" deleteAreYouSure: "確定要刪掉「{x}」嗎?"
resetAreYouSure: "確定要重設嗎?" resetAreYouSure: "確定要重設嗎?"
saved: "已儲存" saved: "已儲存"
messaging: "傳送訊息" messaging: "聊天"
upload: "上傳" upload: "上傳"
keepOriginalUploading: "保留原圖" keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。"
@@ -331,10 +331,10 @@ registration: "註冊"
enableRegistration: "開啟新使用者註冊" enableRegistration: "開啟新使用者註冊"
invite: "邀請" invite: "邀請"
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
inMb: "以Mbps為單位" inMb: "以Mbps為單位"
iconUrl: "圖URL" iconUrl: "圖URL"
bannerUrl: "橫幅圖URL" bannerUrl: "橫幅圖URL"
backgroundImageUrl: "背景圖片的來源網址 " backgroundImageUrl: "背景圖片的來源網址 "
basicInfo: "基本資訊" basicInfo: "基本資訊"
pinnedUsers: "置頂用戶" pinnedUsers: "置頂用戶"
@@ -373,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶"
notesAndReplies: "貼文與回覆" notesAndReplies: "貼文與回覆"
withFiles: "附件" withFiles: "附件"
silence: "禁言" silence: "禁言"
silenceConfirm: "確定要禁言此用戶嗎?" silenceConfirm: "確定要靜音此使用者嗎?"
unsilence: "解除禁言" unsilence: "解除靜音"
unsilenceConfirm: "確定要解除禁言嗎?" unsilenceConfirm: "確定要解除禁言嗎?"
popularUsers: "熱門使用者" popularUsers: "熱門使用者"
recentlyUpdatedUsers: "最近發文的使用者" recentlyUpdatedUsers: "最近發文的使用者"
@@ -383,13 +383,13 @@ recentlyDiscoveredUsers: "最近發現的使用者"
exploreUsersCount: "有{count}個使用者" exploreUsersCount: "有{count}個使用者"
exploreFediverse: "探索聯邦世界" exploreFediverse: "探索聯邦世界"
popularTags: "熱門標籤" popularTags: "熱門標籤"
userList: "清單" userList: "使用者清單"
about: "資訊" about: "關於"
aboutMisskey: "關於 Misskey" aboutMisskey: "關於 Misskey"
administrator: "管理員" administrator: "管理員"
token: "權杖" token: "權杖"
twoStepAuthentication: "兩階段驗證" twoStepAuthentication: "兩階段驗證"
moderator: "監察員" moderator: "審核員"
moderation: "監察" moderation: "監察"
nUsersMentioned: "提到了{n}" nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰" securityKey: "安全金鑰"
@@ -421,7 +421,7 @@ invites: "邀請"
groupName: "群組名稱" groupName: "群組名稱"
members: "成員" members: "成員"
transfer: "轉讓" transfer: "轉讓"
messagingWithUser: "傳送訊息給其他使用者" messagingWithUser: "其他使用者聊天"
messagingWithGroup: "發送訊息至群組" messagingWithGroup: "發送訊息至群組"
title: "標題" title: "標題"
text: "文字" text: "文字"
@@ -473,7 +473,7 @@ createAccount: "建立帳戶"
existingAccount: "現有帳戶" existingAccount: "現有帳戶"
regenerate: "再生" regenerate: "再生"
fontSize: "字體大小" fontSize: "字體大小"
noFollowRequests: "沒有要求跟隨您的請" noFollowRequests: "沒有跟隨您的請"
openImageInNewTab: "於新分頁中開啟圖片" openImageInNewTab: "於新分頁中開啟圖片"
dashboard: "儀表板" dashboard: "儀表板"
local: "本地" local: "本地"
@@ -530,8 +530,8 @@ installedDate: "安裝時間"
lastUsedDate: "最後上線日期" lastUsedDate: "最後上線日期"
state: "狀態" state: "狀態"
sort: "排序" sort: "排序"
ascendingOrder: "昇冪" ascendingOrder: "遞增"
descendingOrder: "降冪" descendingOrder: "遞減"
scratchpad: "暫存記憶體" scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出" output: "輸出"
@@ -995,24 +995,24 @@ _achievements:
_login3: _login3:
title: "初學者Ⅰ" title: "初學者Ⅰ"
description: "總登入天數為3天" description: "總登入天數為3天"
flavor: "從今天開始,我就是Misskeyist" flavor: "從今天開始,我就是Misskist"
_login7: _login7:
title: "初學者ⅠⅠ" title: "初學者ⅠⅠ"
description: "總登入天數為7天" description: "總登入天數為7天"
flavor: "您開始習慣了嗎?" flavor: "您開始習慣了嗎?"
_login15: _login15:
title: "初學者III" title: "初學者ⅠⅠⅠ"
description: "總登入天數為15天" description: "總登入天數為15天"
_login30: _login30:
title: "Misskeyist " title: "Misskist "
description: "總登入天數為30天" description: "總登入天數為30天"
_login60: _login60:
title: "Misskeyist ⅠⅠ" title: "Misskist ⅠⅠ"
description: "總登入天數為60天" description: "總登入天數為60天"
_login100: _login100:
title: "Misskeyist ⅠⅠⅠ" title: "Misskist ⅠⅠⅠ"
description: "總登入天數為100天" description: "總登入天數為100天"
flavor: "辣個 Misskeyist 用戶" flavor: "辣個 Misskist 用戶"
_login200: _login200:
title: "普通Ⅰ" title: "普通Ⅰ"
description: "總登入天數為200天" description: "總登入天數為200天"
@@ -1089,7 +1089,7 @@ _achievements:
title: "請排成一排" title: "請排成一排"
description: "跟隨者超過300人了" description: "跟隨者超過300人了"
_followers500: _followers500:
title: "基" title: "基地台"
description: "超過500名追隨者了" description: "超過500名追隨者了"
_followers1000: _followers1000:
title: "影響者" title: "影響者"
@@ -1111,7 +1111,7 @@ _achievements:
title: "休息一下" title: "休息一下"
description: "用戶端啟動已超過30分鐘" description: "用戶端啟動已超過30分鐘"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "現在沒有" title: "現在沒有"
description: "發文後1分鐘內刪文" description: "發文後1分鐘內刪文"
_postedAtLateNight: _postedAtLateNight:
title: "夜行性" title: "夜行性"
+9 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.2.3", "version": "13.3.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -38,8 +38,8 @@
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
}, },
"resolutions": { "resolutions": {
"chokidar": "^3.5.3", "chokidar": "3.5.3",
"lodash": "^4.17.21" "lodash": "4.17.21"
}, },
"dependencies": { "dependencies": {
"execa": "5.1.1", "execa": "5.1.1",
@@ -49,19 +49,19 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "4.9.4" "typescript": "4.9.5"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.49.0", "@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.49.0", "@typescript-eslint/parser": "5.50.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.5.1",
"eslint": "^8.32.0", "eslint": "8.33.0",
"start-server-and-test": "1.15.3" "start-server-and-test": "1.15.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "^4.2.0" "@tensorflow/tfjs-core": "4.2.0"
} }
} }
@@ -0,0 +1,29 @@
export class cleanup1675404035646 {
name = 'cleanup1675404035646'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTwitterIntegration"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGithubIntegration"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableDiscordIntegration"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerKey"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerSecret"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientId"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientSecret"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientId"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientSecret"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientSecret" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientId" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientSecret" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientId" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerSecret" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerKey" character varying(128)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableDiscordIntegration" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableGithubIntegration" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTwitterIntegration" boolean NOT NULL DEFAULT false`);
}
}
+38 -39
View File
@@ -19,27 +19,27 @@
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs": "^4.2.0", "@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.11.0", "@bull-board/api": "4.11.0",
"@bull-board/fastify": "^4.11.0", "@bull-board/fastify": "4.11.0",
"@bull-board/ui": "^4.11.0", "@bull-board/ui": "4.11.0",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/http-proxy": "^8.4.0", "@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.0", "@fastify/multipart": "7.4.0",
"@fastify/static": "6.6.1", "@fastify/static": "6.8.0",
"@fastify/view": "7.4.0", "@fastify/view": "7.4.1",
"@nestjs/common": "9.2.1", "@nestjs/common": "9.3.1",
"@nestjs/core": "9.2.1", "@nestjs/core": "9.3.1",
"@nestjs/testing": "9.2.1", "@nestjs/testing": "9.3.1",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.0.2",
"accepts": "^1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
@@ -62,11 +62,11 @@
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.2.0", "file-type": "18.2.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "^4.0.0", "form-data": "4.0.0",
"got": "^12.5.3", "got": "12.5.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"ioredis": "4.28.5", "ioredis": "4.28.5",
"ip-cidr": "3.0.11", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.0", "jsdom": "21.1.0",
@@ -75,15 +75,16 @@
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.14", "misskey-js": "0.0.15",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"nodemailer": "6.9.0", "node-fetch": "3.3.0",
"nodemailer": "6.9.1",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "^0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.8.0", "pg": "8.9.0",
"private-ip": "3.0.0", "private-ip": "3.0.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@@ -101,23 +102,22 @@
"rss-parser": "3.12.0", "rss-parser": "3.12.0",
"rxjs": "7.8.0", "rxjs": "7.8.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.8.1", "sanitize-html": "2.9.0",
"seedrandom": "^3.0.5", "seedrandom": "3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.3", "sharp": "0.31.3",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2", "systeminformation": "5.17.8",
"systeminformation": "5.17.3",
"tinycolor2": "1.5.2", "tinycolor2": "1.5.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
"typescript": "4.9.4", "typescript": "4.9.5",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
@@ -125,29 +125,29 @@
"web-push": "3.5.0", "web-push": "3.5.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.12.0", "ws": "8.12.0",
"xev": "3.0.2", "xev": "3.0.2"
"node-fetch": "3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.120", "@jest/globals": "29.4.1",
"@swc/cli": "^0.1.59", "@redocly/openapi-core": "1.0.0-beta.123",
"@swc/core": "1.3.27", "@swc/cli": "0.1.61",
"@swc/core": "1.3.32",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.1",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0", "@types/bull": "4.10.0",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
"@types/color-convert": "^2.0.0", "@types/color-convert": "2.0.0",
"@types/content-disposition": "^0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.2.6", "@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1", "@types/jsdom": "20.0.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.4", "@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
@@ -166,7 +166,6 @@
"@types/sharp": "0.31.1", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.5", "@types/unzipper": "0.10.5",
@@ -175,13 +174,13 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.49.0", "@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.49.0", "@typescript-eslint/parser": "5.50.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.32.0", "eslint": "8.33.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.3.1", "jest": "29.4.1",
"jest-mock": "^29.3.1" "jest-mock": "29.4.1"
} }
} }
+1 -6
View File
@@ -65,11 +65,6 @@ export type Source = {
deliverJobMaxAttempts?: number; deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number; inboxJobMaxAttempts?: number;
syslog: {
host: string;
port: number;
};
mediaProxy?: string; mediaProxy?: string;
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
@@ -113,7 +108,7 @@ const path = process.env.NODE_ENV === 'test'
export function loadConfig() { export function loadConfig() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json') const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
const clientManifest = clientManifestExists ? const clientManifest = clientManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
: { 'src/init.ts': { file: 'src/init.ts' } }; : { 'src/init.ts': { file: 'src/init.ts' } };
-6
View File
@@ -62,7 +62,6 @@ import PerUserNotesChart from './chart/charts/per-user-notes.js';
import PerUserPvChart from './chart/charts/per-user-pv.js'; import PerUserPvChart from './chart/charts/per-user-pv.js';
import DriveChart from './chart/charts/drive.js'; import DriveChart from './chart/charts/drive.js';
import PerUserReactionsChart from './chart/charts/per-user-reactions.js'; import PerUserReactionsChart from './chart/charts/per-user-reactions.js';
import HashtagChart from './chart/charts/hashtag.js';
import PerUserFollowingChart from './chart/charts/per-user-following.js'; import PerUserFollowingChart from './chart/charts/per-user-following.js';
import PerUserDriveChart from './chart/charts/per-user-drive.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js';
import ApRequestChart from './chart/charts/ap-request.js'; import ApRequestChart from './chart/charts/ap-request.js';
@@ -187,7 +186,6 @@ const $PerUserNotesChart: Provider = { provide: 'PerUserNotesChart', useExisting
const $PerUserPvChart: Provider = { provide: 'PerUserPvChart', useExisting: PerUserPvChart }; const $PerUserPvChart: Provider = { provide: 'PerUserPvChart', useExisting: PerUserPvChart };
const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart }; const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart };
const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart }; const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart };
const $HashtagChart: Provider = { provide: 'HashtagChart', useExisting: HashtagChart };
const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart }; const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart };
const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart }; const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart };
const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart }; const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart };
@@ -315,7 +313,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart, PerUserPvChart,
DriveChart, DriveChart,
PerUserReactionsChart, PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart, PerUserFollowingChart,
PerUserDriveChart, PerUserDriveChart,
ApRequestChart, ApRequestChart,
@@ -437,7 +434,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart, $PerUserPvChart,
$DriveChart, $DriveChart,
$PerUserReactionsChart, $PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart, $PerUserFollowingChart,
$PerUserDriveChart, $PerUserDriveChart,
$ApRequestChart, $ApRequestChart,
@@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart, PerUserPvChart,
DriveChart, DriveChart,
PerUserReactionsChart, PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart, PerUserFollowingChart,
PerUserDriveChart, PerUserDriveChart,
ApRequestChart, ApRequestChart,
@@ -680,7 +675,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart, $PerUserPvChart,
$DriveChart, $DriveChart,
$PerUserReactionsChart, $PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart, $PerUserFollowingChart,
$PerUserDriveChart, $PerUserDriveChart,
$ApRequestChart, $ApRequestChart,
+141 -5
View File
@@ -6,22 +6,35 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository } from '@/models/index.js'; import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService {
private cache: Cache<Emoji | null>;
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) { ) {
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
} }
@bindThis @bindThis
@@ -44,12 +57,135 @@ export class CustomEmojiService {
type: data.driveFile.webpublicType ?? data.driveFile.type, type: data.driveFile.webpublicType ?? data.driveFile.type,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
await this.db.queryResultCache!.remove(['meta_emojis']); if (data.host == null) {
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.pack(emoji.id), emoji: await this.emojiEntityService.pack(emoji.id),
}); });
}
return emoji; return emoji;
} }
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
: this.utilityService.isSelfHost(src) ? null // 自ホスト指定
: (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
host = this.utilityService.toPunyNullable(host);
return host;
}
@bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
const name = match[1];
// ホスト正規化
const host = this.utilityService.toPunyNullable(this.normalizeHost(match[2], noteUserHost));
return { name, host };
}
/**
* 添付用(リモート)カスタム絵文字URLを解決する
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
* @returns URL, nullは未マッチを意味する
*/
@bindThis
public async populateEmoji(emojiName: string, noteUserHost: string | null): Promise<string | null> {
const { name, host } = this.parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null;
if (host == null) return null;
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
name,
host: host ?? IsNull(),
})) ?? null;
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
const url = isLocal
? emojiUrl
: this.config.proxyRemoteFiles
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`
: emojiUrl;
return url;
}
/**
* 複数の添付用(リモート)カスタム絵文字URLを解決する (キャシュ付き, 存在しないものは結果から除外される)
*/
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
const res = {} as any;
for (let i = 0; i < emojiNames.length; i++) {
if (emojis[i] != null) {
res[emojiNames[i]] = emojis[i];
}
}
return res;
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/**
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
*/
@bindThis
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
const emojisQuery: any[] = [];
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
if (host == null) continue;
emojisQuery.push({
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
host: host,
});
}
const _emojis = emojisQuery.length > 0 ? await this.emojisRepository.find({
where: emojisQuery,
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}) : [];
for (const emoji of _emojis) {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}
} }
@@ -4,7 +4,6 @@ import type { User } from '@/models/entities/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Hashtag } from '@/models/entities/Hashtag.js'; import type { Hashtag } from '@/models/entities/Hashtag.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; import type { HashtagsRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@@ -20,7 +19,6 @@ export class HashtagService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private hashtagChart: HashtagChart,
) { ) {
} }
@@ -143,9 +141,5 @@ export class HashtagService {
} as Hashtag); } as Hashtag);
} }
} }
if (!isUserAttached) {
this.hashtagChart.update(tag, user);
}
} }
} }
@@ -95,7 +95,7 @@ export class HttpRequestService {
} }
@bindThis @bindThis
public async getJson(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<unknown> { public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
const res = await this.send(url, { const res = await this.send(url, {
method: 'GET', method: 'GET',
headers: Object.assign({ headers: Object.assign({
@@ -106,7 +106,7 @@ export class HttpRequestService {
size: 1024 * 256, size: 1024 * 256,
}); });
return await res.json(); return await res.json() as T;
} }
@bindThis @bindThis
@@ -9,6 +9,14 @@ export type IImage = {
type: string; type: string;
}; };
export type IImageStream = {
data: Readable;
ext: string | null;
type: string;
};
export type IImageStreamable = IImage | IImageStream;
export const webpDefault: sharp.WebpOptions = { export const webpDefault: sharp.WebpOptions = {
quality: 85, quality: 85,
alphaQuality: 95, alphaQuality: 95,
@@ -19,6 +27,7 @@ export const webpDefault: sharp.WebpOptions = {
}; };
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Readable } from 'node:stream';
@Injectable() @Injectable()
export class ImageProcessingService { export class ImageProcessingService {
@@ -64,7 +73,7 @@ export class ImageProcessingService {
*/ */
@bindThis @bindThis
public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
return this.convertSharpToWebp(await sharp(path), width, height, options); return this.convertSharpToWebp(sharp(path), width, height, options);
} }
@bindThis @bindThis
@@ -85,6 +94,27 @@ export class ImageProcessingService {
}; };
} }
@bindThis
public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
return this.convertSharpToWebpStream(sharp(path), width, height, options);
}
@bindThis
public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
const data = sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.webp(options)
return {
data,
ext: 'webp',
type: 'image/webp',
};
}
/** /**
* Convert to PNG * Convert to PNG
* with resize, remove metadata, resolve orientation, stop animation * with resize, remove metadata, resolve orientation, stop animation
+1 -17
View File
@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as SyslogPro from 'syslog-pro';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
@@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions';
@Injectable() @Injectable()
export class LoggerService { export class LoggerService {
private syslogClient;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
) { ) {
if (this.config.syslog) {
this.syslogClient = new SyslogPro.RFC5424({
applicationName: 'Misskey',
timestamp: true,
includeStructuredData: true,
color: true,
extendedColor: true,
server: {
target: config.syslog.host,
port: config.syslog.port,
},
});
}
} }
@bindThis @bindThis
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store, this.syslogClient); return new Logger(domain, color, store);
} }
} }
+2 -8
View File
@@ -9,9 +9,9 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js'; import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js'; import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js'; import { AntennaService } from './AntennaService.js';
import { bindThis } from '@/decorators.js';
import { PushNotificationService } from './PushNotificationService.js'; import { PushNotificationService } from './PushNotificationService.js';
@Injectable() @Injectable()
@@ -107,12 +107,6 @@ export class NoteReadService {
followingChannels: Set<Channel['id']>; followingChannels: Set<Channel['id']>;
}, },
): Promise<void> { ): Promise<void> {
const following = info?.following ? info.following : new Set<string>((await this.followingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({ const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: { where: {
followerId: userId, followerId: userId,
@@ -139,7 +133,7 @@ export class NoteReadService {
if (note.user != null) { // たぶんnullになることは無いはずだけど一応 if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) { for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) { if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note); readAntennaNotes.push(note);
} }
} }
+12 -3
View File
@@ -44,16 +44,25 @@ export class WebhookService implements OnApplicationShutdown {
switch (type) { switch (type) {
case 'webhookCreated': case 'webhookCreated':
if (body.active) { if (body.active) {
this.webhooks.push(body); this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
} }
break; break;
case 'webhookUpdated': case 'webhookUpdated':
if (body.active) { if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id); const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) { if (i > -1) {
this.webhooks[i] = body; this.webhooks[i] = {
...body,
createdAt: new Date(body.createdAt),
};
} else { } else {
this.webhooks.push(body); this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
} }
} else { } else {
this.webhooks = this.webhooks.filter(a => a.id !== body.id); this.webhooks = this.webhooks.filter(a => a.id !== body.id);
@@ -274,7 +274,7 @@ export class ApRendererService {
} as any; } as any;
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replace(/:/g, ''); const name = reaction.replaceAll(':', '');
const emoji = await this.emojisRepository.findOneBy({ const emoji = await this.emojisRepository.findOneBy({
name, name,
host: IsNull(), host: IsNull(),
@@ -1,5 +1,6 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import jsonld from 'jsonld';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js'; import { CONTEXTS } from './misc/contexts.js';
@@ -84,7 +85,9 @@ class LdSignature {
@bindThis @bindThis
public async normalize(data: any) { public async normalize(data: any) {
const customLoader = this.getLoader(); const customLoader = this.getLoader();
return 42; return await jsonld.normalize(data, {
documentLoader: customLoader,
});
} }
@bindThis @bindThis
@@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.js'; import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.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';
@@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
const nameLength = 128; const nameLength = 128;
const summaryLength = 2048; const summaryLength = 2048;
const services: {
[x: string]: (id: string, username: string) => any
} = {
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
'misskey:authentication:github': (id, login) => ({ id, login }),
'misskey:authentication:discord': (id, name) => $discord(id, name),
};
const $discord = (id: string, name: string) => {
if (typeof name !== 'string') {
name = 'unknown#0000';
}
const [username, discriminator] = name.split('#');
return { id, username, discriminator };
};
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
const service = services[source.name];
if (typeof source.value !== 'string') {
source.value = 'unknown';
}
const [id, username] = source.value.split('@');
if (service) {
target[source.name.split(':')[2]] = service(id, username);
}
}
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; private utilityService: UtilityService;
@@ -540,22 +510,16 @@ export class ApPersonService implements OnModuleInit {
name: string, name: string,
value: string value: string
}[] = []; }[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) { for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) { fields.push({
addService(services, attachment.identifier); name: attachment.name,
} else { value: this.mfmService.fromHtml(attachment.value),
fields.push({ });
name: attachment.name,
value: this.mfmService.fromHtml(attachment.value),
});
}
} }
} }
return { fields, services }; return { fields };
} }
@bindThis @bindThis
@@ -10,7 +10,6 @@ import PerUserNotesChart from './charts/per-user-notes.js';
import PerUserPvChart from './charts/per-user-pv.js'; import PerUserPvChart from './charts/per-user-pv.js';
import DriveChart from './charts/drive.js'; import DriveChart from './charts/drive.js';
import PerUserReactionsChart from './charts/per-user-reactions.js'; import PerUserReactionsChart from './charts/per-user-reactions.js';
import HashtagChart from './charts/hashtag.js';
import PerUserFollowingChart from './charts/per-user-following.js'; import PerUserFollowingChart from './charts/per-user-following.js';
import PerUserDriveChart from './charts/per-user-drive.js'; import PerUserDriveChart from './charts/per-user-drive.js';
import ApRequestChart from './charts/ap-request.js'; import ApRequestChart from './charts/ap-request.js';
@@ -31,7 +30,6 @@ export class ChartManagementService implements OnApplicationShutdown {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@@ -46,7 +44,6 @@ export class ChartManagementService implements OnApplicationShutdown {
this.perUserPvChart, this.perUserPvChart,
this.driveChart, this.driveChart,
this.perUserReactionsChart, this.perUserReactionsChart,
this.hashtagChart,
this.perUserFollowingChart, this.perUserFollowingChart,
this.perUserDriveChart, this.perUserDriveChart,
this.apRequestChart, this.apRequestChart,
@@ -1,10 +0,0 @@
import Chart from '../../core.js';
export const name = 'hashtag';
export const schema = {
'local.users': { uniqueIncrement: true },
'remote.users': { uniqueIncrement: true },
} as const;
export const entity = Chart.schemaToEntity(name, schema, true);
@@ -1,45 +0,0 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
import type { User } from '@/models/entities/User.js';
import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/hashtag.js';
import type { KVs } from '../core.js';
/**
* ハッシュタグに関するチャート
*/
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class HashtagChart extends Chart<typeof schema> {
constructor(
@Inject(DI.db)
private db: DataSource,
private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
return {};
}
@bindThis
public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise<void> {
await this.commit({
'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [],
'remote.users': this.userEntityService.isLocalUser(user) ? [] : [user.id],
}, hashtag);
}
}
+23 -23
View File
@@ -11,9 +11,9 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Repository, DataSource } from 'typeorm'; import type { Repository, DataSource } from 'typeorm';
const columnPrefix = '___' as const; const COLUMN_PREFIX = '___' as const;
const uniqueTempColumnPrefix = 'unique_temp___' as const; const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
const columnDot = '_' as const; const COLUMN_DELIMITER = '_' as const;
type Schema = Record<string, { type Schema = Record<string, {
uniqueIncrement?: boolean; uniqueIncrement?: boolean;
@@ -26,14 +26,14 @@ type Schema = Record<string, {
accumulate?: boolean; accumulate?: boolean;
}>; }>;
type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof columnDot}${KeyToColumnName<R2>}` : T; type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof COLUMN_DELIMITER}${KeyToColumnName<R2>}` : T;
type Columns<S extends Schema> = { type Columns<S extends Schema> = {
[K in keyof S as `${typeof columnPrefix}${KeyToColumnName<string & K>}`]: number; [K in keyof S as `${typeof COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: number;
}; };
type TempColumnsForUnique<S extends Schema> = { type TempColumnsForUnique<S extends Schema> = {
[K in keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never; [K in keyof S as `${typeof UNIQUE_TEMP_COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never;
}; };
type RawRecord<S extends Schema> = { type RawRecord<S extends Schema> = {
@@ -138,20 +138,20 @@ export default abstract class Chart<T extends Schema> {
private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> { private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> {
const columns = {} as Record<string, { type: string; array?: boolean; default?: any; }>; const columns = {} as Record<string, { type: string; array?: boolean; default?: any; }>;
for (const [k, v] of Object.entries(schema)) { for (const [k, v] of Object.entries(schema)) {
const name = k.replaceAll('.', columnDot); const name = k.replaceAll('.', COLUMN_DELIMITER);
const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer'; const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer';
if (v.uniqueIncrement) { if (v.uniqueIncrement) {
columns[uniqueTempColumnPrefix + name] = { columns[UNIQUE_TEMP_COLUMN_PREFIX + name] = {
type: 'varchar', type: 'varchar',
array: true, array: true,
default: '{}', default: '{}',
}; };
columns[columnPrefix + name] = { columns[COLUMN_PREFIX + name] = {
type, type,
default: 0, default: 0,
}; };
} else { } else {
columns[columnPrefix + name] = { columns[COLUMN_PREFIX + name] = {
type, type,
default: 0, default: 0,
}; };
@@ -253,8 +253,8 @@ export default abstract class Chart<T extends Schema> {
@bindThis @bindThis
private convertRawRecord(x: RawRecord<T>): KVs<T> { private convertRawRecord(x: RawRecord<T>): KVs<T> {
const kvs = {} as Record<string, number>; const kvs = {} as Record<string, number>;
for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns<T>)[]) { for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k] as unknown as number; kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
} }
return kvs as KVs<T>; return kvs as KVs<T>;
} }
@@ -357,8 +357,8 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<string, number | unknown[]>; const columns = {} as Record<string, number | unknown[]>;
for (const [k, v] of Object.entries(data)) { for (const [k, v] of Object.entries(data)) {
const name = k.replaceAll('.', columnDot); const name = k.replaceAll('.', COLUMN_DELIMITER);
columns[columnPrefix + name] = v; columns[COLUMN_PREFIX + name] = v;
} }
// 新規ログ挿入 // 新規ログ挿入
@@ -419,13 +419,13 @@ export default abstract class Chart<T extends Schema> {
const queryForDay: Record<keyof RawRecord<T>, number | (() => string)> = {} as any; const queryForDay: Record<keyof RawRecord<T>, number | (() => string)> = {} as any;
for (const [k, v] of Object.entries(finalDiffs)) { for (const [k, v] of Object.entries(finalDiffs)) {
if (typeof v === 'number') { if (typeof v === 'number') {
const name = columnPrefix + k.replaceAll('.', columnDot) as string & keyof Columns<T>; const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof Columns<T>;
if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`; if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`;
if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`; if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`;
if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`; if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`;
if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`; if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`;
} else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント } else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as string & keyof TempColumnsForUnique<T>; const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof TempColumnsForUnique<T>;
// TODO: item をSQLエスケープ // TODO: item をSQLエスケープ
const itemsForHour = v.filter(item => !(logHour[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`); const itemsForHour = v.filter(item => !(logHour[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
const itemsForDay = v.filter(item => !(logDay[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`); const itemsForDay = v.filter(item => !(logDay[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
@@ -437,8 +437,8 @@ export default abstract class Chart<T extends Schema> {
// bake unique count // bake unique count
for (const [k, v] of Object.entries(finalDiffs)) { for (const [k, v] of Object.entries(finalDiffs)) {
if (this.schema[k].uniqueIncrement) { if (this.schema[k].uniqueIncrement) {
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>; const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>; const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size; queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size; queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
} }
@@ -449,15 +449,15 @@ export default abstract class Chart<T extends Schema> {
for (const [k, v] of Object.entries(this.schema)) { for (const [k, v] of Object.entries(this.schema)) {
const intersection = v.intersection; const intersection = v.intersection;
if (intersection) { if (intersection) {
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>; const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
const firstKey = intersection[0]; const firstKey = intersection[0];
const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>; const firstTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + firstKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
const firstValues = finalDiffs[firstKey] as string[] | undefined; const firstValues = finalDiffs[firstKey] as string[] | undefined;
const currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]); const currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]);
const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]); const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]);
for (let i = 1; i < intersection.length; i++) { for (let i = 1; i < intersection.length; i++) {
const targetKey = intersection[i]; const targetKey = intersection[i];
const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>; const targetTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + targetKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
const targetValues = finalDiffs[targetKey] as string[] | undefined; const targetValues = finalDiffs[targetKey] as string[] | undefined;
const targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[targetTempColumnName] as unknown as string[])]); const targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[targetTempColumnName] as unknown as string[])]);
const targetValuesForDay = new Set([...(targetValues ?? []), ...(logDay[targetTempColumnName] as unknown as string[])]); const targetValuesForDay = new Set([...(targetValues ?? []), ...(logDay[targetTempColumnName] as unknown as string[])]);
@@ -510,7 +510,7 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<keyof Columns<T>, number>; const columns = {} as Record<keyof Columns<T>, number>;
for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) { for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) {
const name = columnPrefix + (k as string).replaceAll('.', columnDot) as keyof Columns<T>; const name = COLUMN_PREFIX + (k as string).replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
columns[name] = v; columns[name] = v;
} }
@@ -556,7 +556,7 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<keyof TempColumnsForUnique<T>, []>; const columns = {} as Record<keyof TempColumnsForUnique<T>, []>;
for (const [k, v] of Object.entries(this.schema)) { for (const [k, v] of Object.entries(this.schema)) {
if (v.uniqueIncrement) { if (v.uniqueIncrement) {
const name = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>; const name = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
columns[name] = []; columns[name] = [];
} }
} }
@@ -7,7 +7,6 @@ import { entity as PerUserNotesChart } from './charts/entities/per-user-notes.js
import { entity as PerUserPvChart } from './charts/entities/per-user-pv.js'; import { entity as PerUserPvChart } from './charts/entities/per-user-pv.js';
import { entity as DriveChart } from './charts/entities/drive.js'; import { entity as DriveChart } from './charts/entities/drive.js';
import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js'; import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js';
import { entity as HashtagChart } from './charts/entities/hashtag.js';
import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js'; import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js';
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
import { entity as ApRequestChart } from './charts/entities/ap-request.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js';
@@ -27,7 +26,6 @@ export const entities = [
PerUserPvChart.hour, PerUserPvChart.day, PerUserPvChart.hour, PerUserPvChart.day,
DriveChart.hour, DriveChart.day, DriveChart.hour, DriveChart.day,
PerUserReactionsChart.hour, PerUserReactionsChart.day, PerUserReactionsChart.hour, PerUserReactionsChart.day,
HashtagChart.hour, HashtagChart.day,
PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day, PerUserDriveChart.hour, PerUserDriveChart.day,
ApRequestChart.hour, ApRequestChart.day, ApRequestChart.hour, ApRequestChart.day,
@@ -282,7 +282,9 @@ export class NoteEntityService implements OnModuleInit {
: await this.channelsRepository.findOneBy({ id: note.channelId }) : await this.channelsRepository.findOneBy({ id: note.channelId })
: null; : null;
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x.startsWith(':')).map(x => this.reactionService.decodeReaction(x).reaction).map(x => x.replace(/:/g, '')); const reactionEmojiNames = Object.keys(note.reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packed: Packed<'Note'> = await awaitAll({ const packed: Packed<'Note'> = await awaitAll({
id: note.id, id: note.id,
@@ -299,6 +301,8 @@ export class NoteEntityService implements OnModuleInit {
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions), reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds, fileIds: note.fileIds,
files: this.driveFileEntityService.packMany(note.fileIds), files: this.driveFileEntityService.packMany(note.fileIds),
@@ -384,6 +388,8 @@ export class NoteEntityService implements OnModuleInit {
} }
} }
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notes.map(n => this.pack(n, me, { return await Promise.all(notes.map(n => this.pack(n, me, {
...options, ...options,
_hint_: { _hint_: {
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js'; import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js';
@@ -146,6 +146,8 @@ export class NotificationEntityService implements OnModuleInit {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null); myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
} }
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, { return await Promise.all(notifications.map(x => this.pack(x, {
_hintForEachNotes_: { _hintForEachNotes_: {
myReactions: myReactionsMap, myReactions: myReactionsMap,
@@ -413,6 +413,7 @@ export class UserEntityService implements OnModuleInit {
faviconUrl: instance.faviconUrl, faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor, themeColor: instance.themeColor,
} : undefined) : undefined, } : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
...(opts.detail ? { ...(opts.detail ? {
@@ -488,7 +489,6 @@ export class UserEntityService implements OnModuleInit {
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances, mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes, mutingNotificationTypes: profile!.mutingNotificationTypes,
+2 -17
View File
@@ -17,15 +17,13 @@ export default class Logger {
private context: Context; private context: Context;
private parentLogger: Logger | null = null; private parentLogger: Logger | null = null;
private store: boolean; private store: boolean;
private syslogClient: any | null = null;
constructor(context: string, color?: KEYWORD, store = true, syslogClient = null) { constructor(context: string, color?: KEYWORD, store = true) {
this.context = { this.context = {
name: context, name: context,
color: color, color: color,
}; };
this.store = store; this.store = store;
this.syslogClient = syslogClient;
} }
@bindThis @bindThis
@@ -68,20 +66,7 @@ export default class Logger {
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log); console.log(important ? chalk.bold(log) : log);
if (level === 'error' && data) console.log(data);
if (store) {
if (this.syslogClient) {
const send =
level === 'error' ? this.syslogClient.error :
level === 'warning' ? this.syslogClient.warning :
level === 'success' ? this.syslogClient.info :
level === 'debug' ? this.syslogClient.info :
level === 'info' ? this.syslogClient.info :
null as never;
send.bind(this.syslogClient)(message).catch(() => {});
}
}
} }
@bindThis @bindThis
@@ -4,7 +4,7 @@ import { unique } from '@/misc/prelude/array.js';
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
const emojiNodes = mfm.extract(nodes, (node) => { const emojiNodes = mfm.extract(nodes, (node) => {
return (node.type === 'emojiCode' && node.props.name.length <= 100); return (node.type === 'emojiCode' && node.props.name.length <= 100);
}); }) as mfm.MfmEmojiCode[];
return unique(emojiNodes.map(x => x.props.name)); return unique(emojiNodes.map(x => x.props.name));
} }
@@ -2,7 +2,7 @@ import * as mfm from 'mfm-js';
import { unique } from '@/misc/prelude/array.js'; import { unique } from '@/misc/prelude/array.js';
export function extractHashtags(nodes: mfm.MfmNode[]): string[] { export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag'); const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag') as mfm.MfmHashtag[];
const hashtags = unique(hashtagNodes.map(x => x.props.hashtag)); const hashtags = unique(hashtagNodes.map(x => x.props.hashtag));
return hashtags; return hashtags;
+2 -2
View File
@@ -1,14 +1,14 @@
export function nyaize(text: string): string { export function nyaize(text: string): string {
return text return text
// ja-JP // ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US // en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') .replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') .replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') .replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR // ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode( .replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0) match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
)) ))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
+18 -7
View File
@@ -132,12 +132,28 @@ type NullOrUndefined<p extends Schema, T> =
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
// To get union, we use `Foo extends any ? Hoge<Foo> : never` // To get union, we use `Foo extends any ? Hoge<Foo> : never`
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never; type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never; type ArrayUnion<T> = T extends any ? Array<T> : never;
type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ?
p['anyOf'] extends ReadonlyArray<Schema> ?
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
:
ObjType<p['properties'], NonNullable<p['required']>[number]>
:
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
export type SchemaTypeDef<p extends Schema> = export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'null' ? null : p['type'] extends 'null' ? null :
p['type'] extends 'integer' ? number : p['type'] extends 'integer' ? number :
@@ -149,13 +165,7 @@ export type SchemaTypeDef<p extends Schema> =
string string
) : ) :
p['type'] extends 'boolean' ? boolean : p['type'] extends 'boolean' ? boolean :
p['type'] extends 'object' ? ( p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ? ObjType<p['properties'], NonNullable<p['required']>[number]> :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> :
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
) :
p['type'] extends 'array' ? ( p['type'] extends 'array' ? (
p['items'] extends OfSchema ? ( p['items'] extends OfSchema ? (
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
@@ -166,6 +176,7 @@ export type SchemaTypeDef<p extends Schema> =
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] : p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
any[] any[]
) : ) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
any; any;
@@ -279,57 +279,6 @@ export class Meta {
}) })
public swPrivateKey: string | null; public swPrivateKey: string | null;
@Column('boolean', {
default: false,
})
public enableTwitterIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public twitterConsumerKey: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public twitterConsumerSecret: string | null;
@Column('boolean', {
default: false,
})
public enableGithubIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public githubClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public githubClientSecret: string | null;
@Column('boolean', {
default: false,
})
public enableDiscordIntegration: boolean;
@Column('varchar', {
length: 128,
nullable: true,
})
public discordClientId: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public discordClientSecret: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
nullable: true, nullable: true,
@@ -184,11 +184,6 @@ export class UserProfile {
@JoinColumn() @JoinColumn()
public pinnedPage: Page | null; public pinnedPage: Page | null;
@Column('jsonb', {
default: {},
})
public integrations: Record<string, any>;
@Index() @Index()
@Column('boolean', { @Column('boolean', {
default: false, select: false, default: false, select: false,
@@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
integrations: {
type: 'object',
nullable: true, optional: false,
},
mutedWords: { mutedWords: {
type: 'array', type: 'array',
nullable: false, optional: false, nullable: false, optional: false,
@@ -57,8 +57,15 @@ export class AggregateRetentionProcessorService {
usersCount: targetUserIds.length, usersCount: targetUserIds.length,
}); });
// 今日活動したユーザーを全て取得
const activeUsers = await this.usersRepository.findBy({
host: IsNull(),
lastActiveDate: MoreThan(new Date(Date.now() - (1000 * 60 * 60 * 24))),
});
const activeUsersIds = activeUsers.map(u => u.id);
for (const record of pastRecords) { for (const record of pastRecords) {
const retention = record.userIds.filter(id => targetUserIds.includes(id)).length; const retention = record.userIds.filter(id => activeUsersIds.includes(id)).length;
const data = deepClone(record.data); const data = deepClone(record.data);
data[dateKey] = retention; data[dateKey] = retention;
@@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
@@ -37,7 +36,6 @@ export class CleanChartsProcessorService {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@@ -61,7 +59,6 @@ export class CleanChartsProcessorService {
this.perUserPvChart.clean(), this.perUserPvChart.clean(),
this.driveChart.clean(), this.driveChart.clean(),
this.perUserReactionsChart.clean(), this.perUserReactionsChart.clean(),
this.hashtagChart.clean(),
this.perUserFollowingChart.clean(), this.perUserFollowingChart.clean(),
this.perUserDriveChart.clean(), this.perUserDriveChart.clean(),
this.apRequestChart.clean(), this.apRequestChart.clean(),
@@ -11,13 +11,12 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ResyncChartsProcessorService { export class ResyncChartsProcessorService {
@@ -35,7 +34,6 @@ export class ResyncChartsProcessorService {
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import DriveChart from '@/core/chart/charts/drive.js'; import DriveChart from '@/core/chart/charts/drive.js';
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js';
@@ -37,7 +36,6 @@ export class TickChartsProcessorService {
private perUserPvChart: PerUserPvChart, private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart, private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart, private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart, private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart, private apRequestChart: ApRequestChart,
@@ -61,7 +59,6 @@ export class TickChartsProcessorService {
this.perUserPvChart.tick(false), this.perUserPvChart.tick(false),
this.driveChart.tick(false), this.driveChart.tick(false),
this.perUserReactionsChart.tick(false), this.perUserReactionsChart.tick(false),
this.hashtagChart.tick(false),
this.perUserFollowingChart.tick(false), this.perUserFollowingChart.tick(false),
this.perUserDriveChart.tick(false), this.perUserDriveChart.tick(false),
this.apRequestChart.tick(false), this.apRequestChart.tick(false),
+319 -79
View File
@@ -5,14 +5,14 @@ import { Inject, Injectable } from '@nestjs/common';
import fastifyStatic from '@fastify/static'; import fastifyStatic from '@fastify/static';
import rename from 'rename'; import rename from 'rename';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { DriveFilesRepository } from '@/models/index.js'; import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js'; import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js'; import { InternalStorageService } from '@/core/InternalStorageService.js';
import { contentDisposition } from '@/misc/content-disposition.js'; import { contentDisposition } from '@/misc/content-disposition.js';
@@ -20,6 +20,8 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { isMimeImage } from '@/misc/is-mime-image.js';
import sharp from 'sharp';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@@ -70,23 +72,309 @@ export class FileServerService {
serve: false, serve: false,
}); });
fastify.get('/app-default.jpg', (request, reply) => { fastify.get('/files/app-default.jpg', (request, reply) => {
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
reply.header('Content-Type', 'image/jpeg'); reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file); return reply.send(file);
}); });
fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply)); fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(request, reply)); return await this.sendDriveFile(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await this.sendDriveFile(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{
Params: { url: string; };
Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => {
return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
done(); done();
} }
@bindThis
private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) {
this.logger.error(`${err}`);
reply.header('Cache-Control', 'max-age=300');
if (request.query && 'fallback' in request.query) {
return reply.sendFile('/dummy.png', assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
reply.code(err.statusCode);
return;
}
reply.code(500);
return;
}
@bindThis @bindThis
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) { private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
const key = request.params.key; const key = request.params.key;
const file = await this.getFileFromKey(key).then();
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
if (file.state === 'remote') {
const convertFile = async () => {
if (file.fileRole === 'thumbnail') {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
498,
280
);
} else if (file.mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
2048,
2048,
{ ...webpDefault, lossless: true }
)
}
}
return {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
};
const image = await convertFile();
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
}
if (file.fileRole !== 'original') {
const filename = rename(file.file.name, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : undefined,
}).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
return fs.createReadStream(file.path);
} else {
const stream = fs.createReadStream(file.path);
stream.on('error', this.commonReadableHandlerGenerator(reply));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.file.name));
return stream;
}
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
let image: IImageStreamable | null = null;
if ('emoji' in request.query && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} else {
const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({
height: 128,
withoutEnlargement: true,
})
.webp(webpDefault);
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
} else if ('badge' in request.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(file.path)
.resize(96, 96, {
fit: 'inside',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (file.mime === 'image/svg+xml') {
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
if ('cleanup' in file) {
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
if (url.startsWith(`${this.config.url}/files/`)) {
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
return await this.getFileFromKey(key);
}
return await this.downloadAndDetectTypeFromUrl(url);
}
@bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
> {
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'remote',
mime, ext,
path, cleanup,
}
} catch (e) {
cleanup();
throw e;
}
}
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
// Fetch drive file // Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file') const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key }) .where('file.accessKey = :accessKey', { accessKey: key })
@@ -94,89 +382,41 @@ export class FileServerService {
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key }) .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne(); .getOne();
if (file == null) { if (file == null) return '404';
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
const isThumbnail = file.thumbnailAccessKey === key; const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key; const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) { if (!file.storedInternal) {
if (file.isLink && file.uri) { // 期限切れリモートファイル if (!(file.isLink && file.uri)) return '204';
const [path, cleanup] = await createTemp(); const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return {
try { ...result,
await this.downloadService.downloadUrl(file.uri, path); fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
const { mime, ext } = await this.fileInfoService.detectType(path);
const convertFile = async () => {
if (isThumbnail) {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(mime)) {
return await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if (mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(path);
}
}
if (isWebpublic) {
if (['image/svg+xml'].includes(mime)) {
return await this.imageProcessingService.convertToPng(path, 2048, 2048);
}
}
return {
data: fs.readFileSync(path),
ext,
type: mime,
};
};
const image = await convertFile();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (err) {
this.logger.error(`${err}`);
if (err instanceof StatusError && err.isClientError) {
reply.code(err.statusCode);
reply.header('Cache-Control', 'max-age=86400');
} else {
reply.code(500);
reply.header('Cache-Control', 'max-age=300');
}
} finally {
cleanup();
}
return;
} }
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
} }
if (isThumbnail || isWebpublic) { const path = this.internalStorageService.resolvePath(key);
const { mime, ext } = await this.fileInfoService.detectType(this.internalStorageService.resolvePath(key));
const filename = rename(file.name, {
suffix: isThumbnail ? '-thumb' : '-web',
extname: ext ? `.${ext}` : undefined,
}).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream'); if (isThumbnail || isWebpublic) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); const { mime, ext } = await this.fileInfoService.detectType(path);
reply.header('Content-Disposition', contentDisposition('inline', filename)); return {
return this.internalStorageService.read(key); state: 'stored_internal',
} else { fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
const readable = this.internalStorageService.read(file.accessKey!); file,
readable.on('error', this.commonReadableHandlerGenerator(reply)); mime, ext,
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream'); path,
reply.header('Cache-Control', 'max-age=31536000, immutable'); };
reply.header('Content-Disposition', contentDisposition('inline', file.name)); }
return readable;
return {
state: 'stored_internal',
fileRole: 'original',
file,
mime: file.type,
ext: null,
path,
} }
} }
} }
@@ -1,177 +0,0 @@
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import sharp from 'sharp';
import fastifyStatic from '@fastify/static';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { createTemp } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import type { IImage } from '@/core/ImageProcessingService.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../src/server/assets/`;
@Injectable()
export class MediaProxyServerService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
private fileInfoService: FileInfoService,
private downloadService: DownloadService,
private imageProcessingService: ImageProcessingService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray', false);
//this.createServer = this.createServer.bind(this);
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
done();
});
fastify.register(fastifyStatic, {
root: _dirname,
serve: false,
});
fastify.get<{
Params: { url: string; };
Querystring: { url?: string; };
}>('/:url*', async (request, reply) => await this.handler(request, reply));
done();
}
@bindThis
private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
let image: IImage;
if ('emoji' in request.query && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.readFileSync(path),
ext,
type: mime,
};
} else {
const data = await sharp(path, { animated: !('static' in request.query) })
.resize({
height: 128,
withoutEnlargement: true,
})
.webp(webpDefault)
.toBuffer();
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 200, 200);
} else if ('badge' in request.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(path)
.resize(96, 96, {
fit: 'inside',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (mime === 'image/svg+xml') {
image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault);
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
} else {
image = {
data: fs.readFileSync(path),
ext,
type: mime,
};
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (err) {
this.logger.error(`${err}`);
if ('fallback' in request.query) {
return reply.sendFile('/dummy.png', assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
reply.code(err.statusCode);
} else {
reply.code(500);
}
} finally {
cleanup();
}
}
}
@@ -111,9 +111,6 @@ export class NodeinfoServerService {
enableHcaptcha: meta.enableHcaptcha, enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha, enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,
enableEmail: meta.enableEmail, enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker, enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null, proxyAccountName: proxyAccount ? proxyAccount.username : null,
@@ -3,14 +3,10 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js'; import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { MediaProxyServerService } from './MediaProxyServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js'; import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js';
import { GetterService } from './api/GetterService.js'; import { GetterService } from './api/GetterService.js';
import { DiscordServerService } from './api/integration/DiscordServerService.js';
import { GithubServerService } from './api/integration/GithubServerService.js';
import { TwitterServerService } from './api/integration/TwitterServerService.js';
import { ChannelsService } from './api/stream/ChannelsService.js'; import { ChannelsService } from './api/stream/ChannelsService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js';
import { ApiLoggerService } from './api/ApiLoggerService.js'; import { ApiLoggerService } from './api/ApiLoggerService.js';
@@ -51,14 +47,10 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
UrlPreviewService, UrlPreviewService,
ActivityPubServerService, ActivityPubServerService,
FileServerService, FileServerService,
MediaProxyServerService,
NodeinfoServerService, NodeinfoServerService,
ServerService, ServerService,
WellKnownServerService, WellKnownServerService,
GetterService, GetterService,
DiscordServerService,
GithubServerService,
TwitterServerService,
ChannelsService, ChannelsService,
ApiCallService, ApiCallService,
ApiLoggerService, ApiLoggerService,
+1 -4
View File
@@ -20,7 +20,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js'; import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js';
import { MediaProxyServerService } from './MediaProxyServerService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
@@ -48,7 +47,6 @@ export class ServerService {
private wellKnownServerService: WellKnownServerService, private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService, private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService, private fileServerService: FileServerService,
private mediaProxyServerService: MediaProxyServerService,
private clientServerService: ClientServerService, private clientServerService: ClientServerService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private loggerService: LoggerService, private loggerService: LoggerService,
@@ -73,8 +71,7 @@ export class ServerService {
} }
fastify.register(this.apiServerService.createServer, { prefix: '/api' }); fastify.register(this.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer, { prefix: '/files' }); fastify.register(this.fileServerService.createServer);
fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
fastify.register(this.activityPubServerService.createServer); fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer); fastify.register(this.wellKnownServerService.createServer);
@@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
import { ApiCallService } from './ApiCallService.js'; import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js'; import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js'; import { SigninApiService } from './SigninApiService.js';
import { GithubServerService } from './integration/GithubServerService.js';
import { DiscordServerService } from './integration/DiscordServerService.js';
import { TwitterServerService } from './integration/TwitterServerService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable() @Injectable()
@@ -38,9 +35,6 @@ export class ApiServerService {
private apiCallService: ApiCallService, private apiCallService: ApiCallService,
private signupApiService: SignupApiService, private signupApiService: SignupApiService,
private signinApiService: SigninApiService, private signinApiService: SigninApiService,
private githubServerService: GithubServerService,
private discordServerService: DiscordServerService,
private twitterServerService: TwitterServerService,
) { ) {
//this.createServer = this.createServer.bind(this); //this.createServer = this.createServer.bind(this);
} }
@@ -133,10 +127,6 @@ export class ApiServerService {
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.register(this.discordServerService.create);
fastify.register(this.githubServerService.create);
fastify.register(this.twitterServerService.create);
fastify.get('/v1/instance/peers', async (request, reply) => { fastify.get('/v1/instance/peers', async (request, reply) => {
const instances = await this.instancesRepository.find({ const instances = await this.instancesRepository.find({
select: ['host'], select: ['host'],
@@ -97,7 +97,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js'; import * as ep___charts_drive from './endpoints/charts/drive.js';
import * as ep___charts_federation from './endpoints/charts/federation.js'; import * as ep___charts_federation from './endpoints/charts/federation.js';
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
import * as ep___charts_instance from './endpoints/charts/instance.js'; import * as ep___charts_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js'; import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
@@ -433,7 +432,6 @@ const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useCl
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default }; const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default }; const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default }; const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default };
const $charts_hashtag: Provider = { provide: 'ep:charts/hashtag', useClass: ep___charts_hashtag.default };
const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default }; const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default };
const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default }; const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default };
const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default }; const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default };
@@ -773,7 +771,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest, $charts_apRequest,
$charts_drive, $charts_drive,
$charts_federation, $charts_federation,
$charts_hashtag,
$charts_instance, $charts_instance,
$charts_notes, $charts_notes,
$charts_user_drive, $charts_user_drive,
@@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest, $charts_apRequest,
$charts_drive, $charts_drive,
$charts_federation, $charts_federation,
$charts_hashtag,
$charts_instance, $charts_instance,
$charts_notes, $charts_notes,
$charts_user_drive, $charts_user_drive,
@@ -96,7 +96,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js'; import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js'; import * as ep___charts_drive from './endpoints/charts/drive.js';
import * as ep___charts_federation from './endpoints/charts/federation.js'; import * as ep___charts_federation from './endpoints/charts/federation.js';
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
import * as ep___charts_instance from './endpoints/charts/instance.js'; import * as ep___charts_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js'; import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js'; import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
@@ -430,7 +429,6 @@ const eps = [
['charts/ap-request', ep___charts_apRequest], ['charts/ap-request', ep___charts_apRequest],
['charts/drive', ep___charts_drive], ['charts/drive', ep___charts_drive],
['charts/federation', ep___charts_federation], ['charts/federation', ep___charts_federation],
['charts/hashtag', ep___charts_hashtag],
['charts/instance', ep___charts_instance], ['charts/instance', ep___charts_instance],
['charts/notes', ep___charts_notes], ['charts/notes', ep___charts_notes],
['charts/user/drive', ep___charts_user_drive], ['charts/user/drive', ep___charts_user_drive],
@@ -138,18 +138,6 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableTwitterIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableGithubIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableDiscordIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: { enableServiceWorker: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@@ -223,30 +211,6 @@ export const meta = {
optional: true, nullable: true, optional: true, nullable: true,
format: 'id', format: 'id',
}, },
twitterConsumerKey: {
type: 'string',
optional: true, nullable: true,
},
twitterConsumerSecret: {
type: 'string',
optional: true, nullable: true,
},
githubClientId: {
type: 'string',
optional: true, nullable: true,
},
githubClientSecret: {
type: 'string',
optional: true, nullable: true,
},
discordClientId: {
type: 'string',
optional: true, nullable: true,
},
discordClientSecret: {
type: 'string',
optional: true, nullable: true,
},
summaryProxy: { summaryProxy: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,
@@ -389,9 +353,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
defaultLightTheme: instance.defaultLightTheme, defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme, defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable: instance.deeplAuthKey != null,
pinnedPages: instance.pinnedPages, pinnedPages: instance.pinnedPages,
@@ -409,12 +370,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
proxyAccountId: instance.proxyAccountId, proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
githubClientId: instance.githubClientId,
githubClientSecret: instance.githubClientSecret,
discordClientId: instance.discordClientId,
discordClientSecret: instance.discordClientSecret,
summalyProxy: instance.summalyProxy, summalyProxy: instance.summalyProxy,
email: instance.email, email: instance.email,
smtpSecure: instance.smtpSecure, smtpSecure: instance.smtpSecure,
@@ -65,11 +65,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}; };
} }
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
Object.keys(profile.integrations).forEach(integration => {
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
});
const signins = await this.signinsRepository.findBy({ userId: user.id }); const signins = await this.signinsRepository.findBy({ userId: user.id });
const roles = await this.roleService.getUserRoles(user.id); const roles = await this.roleService.getUserRoles(user.id);
@@ -84,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
carefulBot: profile.carefulBot, carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote, injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail, receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords, mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances, mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes, mutingNotificationTypes: profile.mutingNotificationTypes,
@@ -68,15 +68,6 @@ export const paramDef = {
summalyProxy: { type: 'string', nullable: true }, summalyProxy: { type: 'string', nullable: true },
deeplAuthKey: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' }, deeplIsPro: { type: 'boolean' },
enableTwitterIntegration: { type: 'boolean' },
twitterConsumerKey: { type: 'string', nullable: true },
twitterConsumerSecret: { type: 'string', nullable: true },
enableGithubIntegration: { type: 'boolean' },
githubClientId: { type: 'string', nullable: true },
githubClientSecret: { type: 'string', nullable: true },
enableDiscordIntegration: { type: 'boolean' },
discordClientId: { type: 'string', nullable: true },
discordClientSecret: { type: 'string', nullable: true },
enableEmail: { type: 'boolean' }, enableEmail: { type: 'boolean' },
email: { type: 'string', nullable: true }, email: { type: 'string', nullable: true },
smtpSecure: { type: 'boolean' }, smtpSecure: { type: 'boolean' },
@@ -270,42 +261,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.summalyProxy = ps.summalyProxy; set.summalyProxy = ps.summalyProxy;
} }
if (ps.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration;
}
if (ps.twitterConsumerKey !== undefined) {
set.twitterConsumerKey = ps.twitterConsumerKey;
}
if (ps.twitterConsumerSecret !== undefined) {
set.twitterConsumerSecret = ps.twitterConsumerSecret;
}
if (ps.enableGithubIntegration !== undefined) {
set.enableGithubIntegration = ps.enableGithubIntegration;
}
if (ps.githubClientId !== undefined) {
set.githubClientId = ps.githubClientId;
}
if (ps.githubClientSecret !== undefined) {
set.githubClientSecret = ps.githubClientSecret;
}
if (ps.enableDiscordIntegration !== undefined) {
set.enableDiscordIntegration = ps.enableDiscordIntegration;
}
if (ps.discordClientId !== undefined) {
set.discordClientId = ps.discordClientId;
}
if (ps.discordClientSecret !== undefined) {
set.discordClientSecret = ps.discordClientSecret;
}
if (ps.enableEmail !== undefined) { if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail; set.enableEmail = ps.enableEmail;
} }
@@ -1,37 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { getJsonSchema } from '@/core/chart/core.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import HashtagChart from '@/core/chart/charts/hashtag.js';
import { schema } from '@/core/chart/charts/entities/hashtag.js';
export const meta = {
tags: ['charts', 'hashtags'],
res: getJsonSchema(schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {
type: 'object',
properties: {
span: { type: 'string', enum: ['day', 'hour'] },
limit: { type: 'integer', minimum: 1, maximum: 500, default: 30 },
offset: { type: 'integer', nullable: true, default: null },
tag: { type: 'string' },
},
required: ['span', 'tag'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private hashtagChart: HashtagChart,
) {
super(meta, paramDef, async (ps, me) => {
return await this.hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag);
});
}
}
@@ -169,18 +169,6 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableTwitterIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableGithubIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableDiscordIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: { enableServiceWorker: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@@ -225,18 +213,6 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
twitter: {
type: 'boolean',
optional: false, nullable: false,
},
github: {
type: 'boolean',
optional: false, nullable: false,
},
discord: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: { serviceWorker: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@@ -325,11 +301,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
})), })),
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable: instance.deeplAuthKey != null,
@@ -358,9 +329,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
recaptcha: instance.enableRecaptcha, recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile, turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage, objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: instance.enableServiceWorker, serviceWorker: instance.enableServiceWorker,
miauth: true, miauth: true,
}; };
@@ -90,48 +90,13 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: { visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
cw: { type: 'string', nullable: true, maxLength: 100 }, cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false }, localOnly: { type: 'boolean', default: false },
noExtractMentions: { type: 'boolean', default: false }, noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false },
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
replyId: { type: 'string', format: 'misskey:id', nullable: true }, replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true },
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean', default: false },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
}, },
anyOf: [ anyOf: [
{ {
@@ -143,21 +108,60 @@ export const paramDef = {
}, },
{ {
// (re)note with files, text and poll are optional // (re)note with files, text and poll are optional
properties: {
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['fileIds'], required: ['fileIds'],
}, },
{ {
// (re)note with files, text and poll are optional // (re)note with files, text and poll are optional
properties: {
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
},
required: ['mediaIds'], required: ['mediaIds'],
}, },
{ {
// (re)note with poll, text and files are optional // (re)note with poll, text and files are optional
properties: { properties: {
poll: { type: 'object', nullable: false }, poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
}, },
required: ['poll'], required: ['poll'],
}, },
{ {
// pure renote // pure renote
properties: {
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: ['renoteId'], required: ['renoteId'],
}, },
], ],
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
if (me == null || (me.id !== ps.userId && !profile.publicReactions)) { if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic); throw new ApiError(meta.errors.reactionsNotPublic);
} }
@@ -29,14 +29,22 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
username: { type: 'string', nullable: true },
host: { type: 'string', nullable: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true }, detail: { type: 'boolean', default: true },
}, },
anyOf: [ anyOf: [
{ required: ['username'] }, {
{ required: ['host'] }, properties: {
username: { type: 'string', nullable: true },
},
required: ['username']
},
{
properties: {
host: { type: 'string', nullable: true },
},
required: ['host']
},
], ],
} as const; } as const;
@@ -1,308 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class DiscordServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/discord', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.discord;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'Discordの連携を解除しました :v:';
});
const getOAuth2 = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableDiscordIntegration) {
return new OAuth2(
meta.discordClientId!,
meta.discordClientSecret!,
'https://discord.com/',
'api/oauth2/authorize',
'api/oauth2/token');
} else {
return null;
}
};
fastify.get('/connect/discord', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const params = {
redirect_uri: `${this.config.url}/api/dc/cb`,
scope: ['identify'],
state: uuid(),
response_type: 'code',
};
this.redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOAuth2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/signin/discord', async (request, reply) => {
const sessid = uuid();
const params = {
redirect_uri: `${this.config.url}/api/dc/cb`,
scope: ['identify'],
state: uuid(),
response_type: 'code',
};
reply.setCookie('signin_with_discord_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
this.redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOAuth2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/dc/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOAuth2();
if (!userToken) {
const sessid = request.cookies['signin_with_discord_sid'];
if (!sessid) {
throw new FastifyReplyError(400, 'invalid session');
}
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
grant_type: 'authorization_code',
redirect_uri,
}, (err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
}));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const profile = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'discord\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (profile == null) {
throw new FastifyReplyError(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
}
await this.userProfilesRepository.update(profile.userId, {
integrations: {
...profile.integrations,
discord: {
id: id,
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
username: username,
discriminator: discriminator,
},
},
});
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: profile.userId }) as ILocalUser, true);
} else {
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
grant_type: 'authorization_code',
redirect_uri,
}, (err, accessToken, refreshToken, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({
accessToken,
refreshToken,
expiresDate: Date.now() + Number(result.expires_in) * 1000,
});
}
}));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
discord: {
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
id: id,
username: username,
discriminator: discriminator,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}
@@ -1,280 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class GithubServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/github', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.github;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'GitHubの連携を解除しました :v:';
});
const getOath2 = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
return new OAuth2(
meta.githubClientId,
meta.githubClientSecret,
'https://github.com/',
'login/oauth/authorize',
'login/oauth/access_token');
} else {
return null;
}
};
fastify.get('/connect/github', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (!userToken) {
throw new FastifyReplyError(400, 'signin required');
}
const params = {
redirect_uri: `${this.config.url}/api/gh/cb`,
scope: ['read:user'],
state: uuid(),
};
this.redisClient.set(userToken, JSON.stringify(params));
const oauth2 = await getOath2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/signin/github', async (request, reply) => {
const sessid = uuid();
const params = {
redirect_uri: `${this.config.url}/api/gh/cb`,
scope: ['read:user'],
state: uuid(),
};
reply.setCookie('signin_with_github_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
this.redisClient.set(sessid, JSON.stringify(params));
const oauth2 = await getOath2();
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
fastify.get('/gh/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOath2();
if (!userToken) {
const sessid = request.cookies['signin_with_github_sid'];
if (!sessid) {
throw new FastifyReplyError(400, 'invalid session');
}
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
oauth2!.getOAuthAccessToken(code, {
redirect_uri,
}, (err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
}));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const link = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'github\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
throw new FastifyReplyError(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
}
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
} else {
const code = request.query.code;
if (!code || typeof code !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, state) => {
if (state == null) throw new Error('empty state');
res(JSON.parse(state));
});
});
if (request.query.state !== state) {
throw new FastifyReplyError(400, 'invalid session');
}
const { accessToken } = await new Promise<{ accessToken: string }>((res, rej) =>
oauth2!.getOAuthAccessToken(
code,
{ redirect_uri },
(err, accessToken, refresh, result) => {
if (err) {
rej(err);
} else if (result.error) {
rej(result.error);
} else {
res({ accessToken });
}
}));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'number') {
throw new FastifyReplyError(400, 'invalid session');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
github: {
accessToken: accessToken,
id: id,
login: login,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}
@@ -1,225 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import * as autwh from 'autwh';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from '../SigninService.js';
import type { FastifyInstance, FastifyRequest, FastifyPluginOptions } from 'fastify';
@Injectable()
export class TwitterServerService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private userEntityService: UserEntityService,
private httpRequestService: HttpRequestService,
private globalEventService: GlobalEventService,
private metaService: MetaService,
private signinService: SigninService,
) {
//this.create = this.create.bind(this);
}
@bindThis
public create(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/disconnect/twitter', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (userToken == null) {
throw new FastifyReplyError(400, 'signin required');
}
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
delete profile.integrations.twitter;
await this.userProfilesRepository.update(user.id, {
integrations: profile.integrations,
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return 'Twitterの連携を解除しました :v:';
});
const getTwAuth = async () => {
const meta = await this.metaService.fetch(true);
if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
return autwh({
consumerKey: meta.twitterConsumerKey,
consumerSecret: meta.twitterConsumerSecret,
callbackUrl: `${this.config.url}/api/tw/cb`,
});
} else {
return null;
}
};
fastify.get('/connect/twitter', async (request, reply) => {
if (!this.compareOrigin(request)) {
throw new FastifyReplyError(400, 'invalid origin');
}
const userToken = this.getUserToken(request);
if (userToken == null) {
throw new FastifyReplyError(400, 'signin required');
}
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
this.redisClient.set(userToken, JSON.stringify(twCtx));
reply.redirect(twCtx.url);
});
fastify.get('/signin/twitter', async (request, reply) => {
const twAuth = await getTwAuth();
const twCtx = await twAuth!.begin();
const sessid = uuid();
this.redisClient.set(sessid, JSON.stringify(twCtx));
reply.setCookie('signin_with_twitter_sid', sessid, {
path: '/',
secure: this.config.url.startsWith('https'),
httpOnly: true,
});
reply.redirect(twCtx.url);
});
fastify.get('/tw/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const twAuth = await getTwAuth();
if (userToken == null) {
const sessid = request.cookies['signin_with_twitter_sid'];
if (sessid == null) {
throw new FastifyReplyError(400, 'invalid session');
}
const get = new Promise<any>((res, rej) => {
this.redisClient.get(sessid, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const verifier = request.query.oauth_verifier;
if (!verifier || typeof verifier !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const link = await this.userProfilesRepository.createQueryBuilder()
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
.andWhere('"userHost" IS NULL')
.getOne();
if (link == null) {
throw new FastifyReplyError(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
}
return this.signinService.signin(request, reply, await this.usersRepository.findOneBy({ id: link.userId }) as ILocalUser, true);
} else {
const verifier = request.query.oauth_verifier;
if (!verifier || typeof verifier !== 'string') {
throw new FastifyReplyError(400, 'invalid session');
}
const get = new Promise<any>((res, rej) => {
this.redisClient.get(userToken, async (_, twCtx) => {
res(twCtx);
});
});
const twCtx = await get;
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const user = await this.usersRepository.findOneByOrFail({
host: IsNull(),
token: userToken,
});
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
await this.userProfilesRepository.update(user.id, {
integrations: {
...profile.integrations,
twitter: {
accessToken: result.accessToken,
accessTokenSecret: result.accessTokenSecret,
userId: result.userId,
screenName: result.screenName,
},
},
});
// Publish i updated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', await this.userEntityService.pack(user, user, {
detail: true,
includeSecrets: true,
}));
return `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
}
});
done();
}
@bindThis
private getUserToken(request: FastifyRequest): string | null {
return ((request.headers['cookie'] ?? '').match(/igi=(\w+)/) ?? [null, null])[1];
}
@bindThis
private compareOrigin(request: FastifyRequest): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = request.headers['referer'];
return (normalizeUrl(referer) === normalizeUrl(this.config.url));
}
}
+41 -37
View File
@@ -18,31 +18,26 @@ import { Following, Role, RoleAssignment } from '@/models';
import type Emitter from 'strict-event-emitter-types'; import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
// redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K];
};
//#region Stream type-body definitions //#region Stream type-body definitions
export interface InternalStreamTypes { export interface InternalStreamTypes {
userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>; userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; };
userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>; userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
remoteUserUpdated: Serialized<{ id: User['id']; }>; remoteUserUpdated: { id: User['id']; };
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; follow: { followerId: User['id']; followeeId: User['id']; };
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; unfollow: { followerId: User['id']; followeeId: User['id']; };
policiesUpdated: Serialized<Role['policies']>; policiesUpdated: Role['policies'];
roleCreated: Serialized<Role>; roleCreated: Role;
roleDeleted: Serialized<Role>; roleDeleted: Role;
roleUpdated: Serialized<Role>; roleUpdated: Role;
userRoleAssigned: Serialized<RoleAssignment>; userRoleAssigned: RoleAssignment;
userRoleUnassigned: Serialized<RoleAssignment>; userRoleUnassigned: RoleAssignment;
webhookCreated: Serialized<Webhook>; webhookCreated: Webhook;
webhookDeleted: Serialized<Webhook>; webhookDeleted: Webhook;
webhookUpdated: Serialized<Webhook>; webhookUpdated: Webhook;
antennaCreated: Serialized<Antenna>; antennaCreated: Antenna;
antennaDeleted: Serialized<Antenna>; antennaDeleted: Antenna;
antennaUpdated: Serialized<Antenna>; antennaUpdated: Antenna;
metaUpdated: Serialized<Meta>; metaUpdated: Meta;
} }
export interface BroadcastTypes { export interface BroadcastTypes {
@@ -210,63 +205,72 @@ type EventUnionFromDictionary<
U = Events<T> U = Events<T>
> = U[keyof U]; > = U[keyof U];
// redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K] extends Record<string, any> ? Serialized<T[K]> : T[K];
};
type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>;
};
// name/messages(spec) pairs dictionary // name/messages(spec) pairs dictionary
export type StreamMessages = { export type StreamMessages = {
internal: { internal: {
name: 'internal'; name: 'internal';
payload: EventUnionFromDictionary<InternalStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
}; };
broadcast: { broadcast: {
name: 'broadcast'; name: 'broadcast';
payload: EventUnionFromDictionary<BroadcastTypes>; payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
}; };
user: { user: {
name: `user:${User['id']}`; name: `user:${User['id']}`;
payload: EventUnionFromDictionary<UserStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<UserStreamTypes>>;
}; };
main: { main: {
name: `mainStream:${User['id']}`; name: `mainStream:${User['id']}`;
payload: EventUnionFromDictionary<MainStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
}; };
drive: { drive: {
name: `driveStream:${User['id']}`; name: `driveStream:${User['id']}`;
payload: EventUnionFromDictionary<DriveStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
}; };
note: { note: {
name: `noteStream:${Note['id']}`; name: `noteStream:${Note['id']}`;
payload: EventUnionFromDictionary<NoteStreamEventTypes>; payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
}; };
channel: { channel: {
name: `channelStream:${Channel['id']}`; name: `channelStream:${Channel['id']}`;
payload: EventUnionFromDictionary<ChannelStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<ChannelStreamTypes>>;
}; };
userList: { userList: {
name: `userListStream:${UserList['id']}`; name: `userListStream:${UserList['id']}`;
payload: EventUnionFromDictionary<UserListStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
}; };
antenna: { antenna: {
name: `antennaStream:${Antenna['id']}`; name: `antennaStream:${Antenna['id']}`;
payload: EventUnionFromDictionary<AntennaStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
}; };
messaging: { messaging: {
name: `messagingStream:${User['id']}-${User['id']}`; name: `messagingStream:${User['id']}-${User['id']}`;
payload: EventUnionFromDictionary<MessagingStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<MessagingStreamTypes>>;
}; };
groupMessaging: { groupMessaging: {
name: `messagingStream:${UserGroup['id']}`; name: `messagingStream:${UserGroup['id']}`;
payload: EventUnionFromDictionary<GroupMessagingStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<GroupMessagingStreamTypes>>;
}; };
messagingIndex: { messagingIndex: {
name: `messagingIndexStream:${User['id']}`; name: `messagingIndexStream:${User['id']}`;
payload: EventUnionFromDictionary<MessagingIndexStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<MessagingIndexStreamTypes>>;
}; };
admin: { admin: {
name: `adminStream:${User['id']}`; name: `adminStream:${User['id']}`;
payload: EventUnionFromDictionary<AdminStreamTypes>; payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
}; };
notes: { notes: {
name: 'notesStream'; name: 'notesStream';
payload: Packed<'Note'>; payload: Serialized<Packed<'Note'>>;
}; };
}; };
+67 -67
View File
@@ -100,90 +100,90 @@ describe('API visibility', () => {
//#region show post //#region show post
// public // public
it('[show] public-postを自分が見れる', async () => { test('[show] public-postを自分が見れる', async () => {
const res = await show(pub.id, alice); const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-postをフォロワーが見れる', async () => { test('[show] public-postをフォロワーが見れる', async () => {
const res = await show(pub.id, follower); const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-postを非フォロワーが見れる', async () => { test('[show] public-postを非フォロワーが見れる', async () => {
const res = await show(pub.id, other); const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-postを未認証が見れる', async () => { test('[show] public-postを未認証が見れる', async () => {
const res = await show(pub.id, null); const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
// home // home
it('[show] home-postを自分が見れる', async () => { test('[show] home-postを自分が見れる', async () => {
const res = await show(home.id, alice); const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-postをフォロワーが見れる', async () => { test('[show] home-postをフォロワーが見れる', async () => {
const res = await show(home.id, follower); const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-postを非フォロワーが見れる', async () => { test('[show] home-postを非フォロワーが見れる', async () => {
const res = await show(home.id, other); const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-postを未認証が見れる', async () => { test('[show] home-postを未認証が見れる', async () => {
const res = await show(home.id, null); const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
// followers // followers
it('[show] followers-postを自分が見れる', async () => { test('[show] followers-postを自分が見れる', async () => {
const res = await show(fol.id, alice); const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] followers-postをフォロワーが見れる', async () => { test('[show] followers-postをフォロワーが見れる', async () => {
const res = await show(fol.id, follower); const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] followers-postを非フォロワーが見れない', async () => { test('[show] followers-postを非フォロワーが見れない', async () => {
const res = await show(fol.id, other); const res = await show(fol.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] followers-postを未認証が見れない', async () => { test('[show] followers-postを未認証が見れない', async () => {
const res = await show(fol.id, null); const res = await show(fol.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
// specified // specified
it('[show] specified-postを自分が見れる', async () => { test('[show] specified-postを自分が見れる', async () => {
const res = await show(spe.id, alice); const res = await show(spe.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] specified-postを指定ユーザーが見れる', async () => { test('[show] specified-postを指定ユーザーが見れる', async () => {
const res = await show(spe.id, target); const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] specified-postをフォロワーが見れない', async () => { test('[show] specified-postをフォロワーが見れない', async () => {
const res = await show(spe.id, follower); const res = await show(spe.id, follower);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-postを非フォロワーが見れない', async () => { test('[show] specified-postを非フォロワーが見れない', async () => {
const res = await show(spe.id, other); const res = await show(spe.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-postを未認証が見れない', async () => { test('[show] specified-postを未認証が見れない', async () => {
const res = await show(spe.id, null); const res = await show(spe.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@@ -191,110 +191,110 @@ describe('API visibility', () => {
//#region show reply //#region show reply
// public // public
it('[show] public-replyを自分が見れる', async () => { test('[show] public-replyを自分が見れる', async () => {
const res = await show(pubR.id, alice); const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-replyをされた人が見れる', async () => { test('[show] public-replyをされた人が見れる', async () => {
const res = await show(pubR.id, target); const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-replyをフォロワーが見れる', async () => { test('[show] public-replyをフォロワーが見れる', async () => {
const res = await show(pubR.id, follower); const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-replyを非フォロワーが見れる', async () => { test('[show] public-replyを非フォロワーが見れる', async () => {
const res = await show(pubR.id, other); const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] public-replyを未認証が見れる', async () => { test('[show] public-replyを未認証が見れる', async () => {
const res = await show(pubR.id, null); const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
// home // home
it('[show] home-replyを自分が見れる', async () => { test('[show] home-replyを自分が見れる', async () => {
const res = await show(homeR.id, alice); const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-replyをされた人が見れる', async () => { test('[show] home-replyをされた人が見れる', async () => {
const res = await show(homeR.id, target); const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-replyをフォロワーが見れる', async () => { test('[show] home-replyをフォロワーが見れる', async () => {
const res = await show(homeR.id, follower); const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-replyを非フォロワーが見れる', async () => { test('[show] home-replyを非フォロワーが見れる', async () => {
const res = await show(homeR.id, other); const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] home-replyを未認証が見れる', async () => { test('[show] home-replyを未認証が見れる', async () => {
const res = await show(homeR.id, null); const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
// followers // followers
it('[show] followers-replyを自分が見れる', async () => { test('[show] followers-replyを自分が見れる', async () => {
const res = await show(folR.id, alice); const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => { test('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => {
const res = await show(folR.id, target); const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] followers-replyをフォロワーが見れる', async () => { test('[show] followers-replyをフォロワーが見れる', async () => {
const res = await show(folR.id, follower); const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] followers-replyを非フォロワーが見れない', async () => { test('[show] followers-replyを非フォロワーが見れない', async () => {
const res = await show(folR.id, other); const res = await show(folR.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] followers-replyを未認証が見れない', async () => { test('[show] followers-replyを未認証が見れない', async () => {
const res = await show(folR.id, null); const res = await show(folR.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
// specified // specified
it('[show] specified-replyを自分が見れる', async () => { test('[show] specified-replyを自分が見れる', async () => {
const res = await show(speR.id, alice); const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] specified-replyを指定ユーザーが見れる', async () => { test('[show] specified-replyを指定ユーザーが見れる', async () => {
const res = await show(speR.id, target); const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] specified-replyをされた人が指定されてなくても見れる', async () => { test('[show] specified-replyをされた人が指定されてなくても見れる', async () => {
const res = await show(speR.id, target); const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
}); });
it('[show] specified-replyをフォロワーが見れない', async () => { test('[show] specified-replyをフォロワーが見れない', async () => {
const res = await show(speR.id, follower); const res = await show(speR.id, follower);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-replyを非フォロワーが見れない', async () => { test('[show] specified-replyを非フォロワーが見れない', async () => {
const res = await show(speR.id, other); const res = await show(speR.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-replyを未認証が見れない', async () => { test('[show] specified-replyを未認証が見れない', async () => {
const res = await show(speR.id, null); const res = await show(speR.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
@@ -302,131 +302,131 @@ describe('API visibility', () => {
//#region show mention //#region show mention
// public // public
it('[show] public-mentionを自分が見れる', async () => { test('[show] public-mentionを自分が見れる', async () => {
const res = await show(pubM.id, alice); const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] public-mentionをされた人が見れる', async () => { test('[show] public-mentionをされた人が見れる', async () => {
const res = await show(pubM.id, target); const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] public-mentionをフォロワーが見れる', async () => { test('[show] public-mentionをフォロワーが見れる', async () => {
const res = await show(pubM.id, follower); const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] public-mentionを非フォロワーが見れる', async () => { test('[show] public-mentionを非フォロワーが見れる', async () => {
const res = await show(pubM.id, other); const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] public-mentionを未認証が見れる', async () => { test('[show] public-mentionを未認証が見れる', async () => {
const res = await show(pubM.id, null); const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
// home // home
it('[show] home-mentionを自分が見れる', async () => { test('[show] home-mentionを自分が見れる', async () => {
const res = await show(homeM.id, alice); const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] home-mentionをされた人が見れる', async () => { test('[show] home-mentionをされた人が見れる', async () => {
const res = await show(homeM.id, target); const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] home-mentionをフォロワーが見れる', async () => { test('[show] home-mentionをフォロワーが見れる', async () => {
const res = await show(homeM.id, follower); const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] home-mentionを非フォロワーが見れる', async () => { test('[show] home-mentionを非フォロワーが見れる', async () => {
const res = await show(homeM.id, other); const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] home-mentionを未認証が見れる', async () => { test('[show] home-mentionを未認証が見れる', async () => {
const res = await show(homeM.id, null); const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
// followers // followers
it('[show] followers-mentionを自分が見れる', async () => { test('[show] followers-mentionを自分が見れる', async () => {
const res = await show(folM.id, alice); const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => { test('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => {
const res = await show(folM.id, target); const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] followers-mentionをフォロワーが見れる', async () => { test('[show] followers-mentionをフォロワーが見れる', async () => {
const res = await show(folM.id, follower); const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
}); });
it('[show] followers-mentionを非フォロワーが見れない', async () => { test('[show] followers-mentionを非フォロワーが見れない', async () => {
const res = await show(folM.id, other); const res = await show(folM.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] followers-mentionを未認証が見れない', async () => { test('[show] followers-mentionを未認証が見れない', async () => {
const res = await show(folM.id, null); const res = await show(folM.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
// specified // specified
it('[show] specified-mentionを自分が見れる', async () => { test('[show] specified-mentionを自分が見れる', async () => {
const res = await show(speM.id, alice); const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x'); assert.strictEqual(res.body.text, '@target2 x');
}); });
it('[show] specified-mentionを指定ユーザーが見れる', async () => { test('[show] specified-mentionを指定ユーザーが見れる', async () => {
const res = await show(speM.id, target); const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x'); assert.strictEqual(res.body.text, '@target2 x');
}); });
it('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => { test('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => {
const res = await show(speM.id, target2); const res = await show(speM.id, target2);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-mentionをフォロワーが見れない', async () => { test('[show] specified-mentionをフォロワーが見れない', async () => {
const res = await show(speM.id, follower); const res = await show(speM.id, follower);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-mentionを非フォロワーが見れない', async () => { test('[show] specified-mentionを非フォロワーが見れない', async () => {
const res = await show(speM.id, other); const res = await show(speM.id, other);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
it('[show] specified-mentionを未認証が見れない', async () => { test('[show] specified-mentionを未認証が見れない', async () => {
const res = await show(speM.id, null); const res = await show(speM.id, null);
assert.strictEqual(res.body.isHidden, true); assert.strictEqual(res.body.isHidden, true);
}); });
//#endregion //#endregion
//#region HTL //#region HTL
it('[HTL] public-post が 自分が見れる', async () => { test('[HTL] public-post が 自分が見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice); const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
it('[HTL] public-post が 非フォロワーから見れない', async () => { test('[HTL] public-post が 非フォロワーから見れない', async () => {
const res = await request('/notes/timeline', { limit: 100 }, other); const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === pub.id); const notes = res.body.filter((n: any) => n.id === pub.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
it('[HTL] followers-post が フォロワーから見れる', async () => { test('[HTL] followers-post が フォロワーから見れる', async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower); const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === fol.id); const notes = res.body.filter((n: any) => n.id === fol.id);
@@ -435,21 +435,21 @@ describe('API visibility', () => {
//#endregion //#endregion
//#region RTL //#region RTL
it('[replies] followers-reply が フォロワーから見れる', async () => { test('[replies] followers-reply が フォロワーから見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes.length, 0); assert.strictEqual(notes.length, 0);
}); });
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
@@ -458,14 +458,14 @@ describe('API visibility', () => {
//#endregion //#endregion
//#region MTL //#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target); const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folR.id); const notes = res.body.filter((n: any) => n.id === folR.id);
assert.strictEqual(notes[0].text, 'x'); assert.strictEqual(notes[0].text, 'x');
}); });
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
const res = await request('/notes/mentions', { limit: 100 }, target); const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id === folM.id); const notes = res.body.filter((n: any) => n.id === folM.id);
+7 -7
View File
@@ -22,7 +22,7 @@ describe('API', () => {
}); });
describe('General validation', () => { describe('General validation', () => {
it('wrong type', async(async () => { test('wrong type', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
string: 42, string: 42,
@@ -30,14 +30,14 @@ describe('API', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('missing require param', async(async () => { test('missing require param', async(async () => {
const res = await request('/test', { const res = await request('/test', {
string: 'a', string: 'a',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('invalid misskey:id (empty string)', async(async () => { test('invalid misskey:id (empty string)', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
id: '', id: '',
@@ -45,7 +45,7 @@ describe('API', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('valid misskey:id', async(async () => { test('valid misskey:id', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
id: '8wvhjghbxu', id: '8wvhjghbxu',
@@ -53,7 +53,7 @@ describe('API', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
})); }));
it('default value', async(async () => { test('default value', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
string: 'a', string: 'a',
@@ -62,7 +62,7 @@ describe('API', () => {
assert.strictEqual(res.body.default, 'hello'); assert.strictEqual(res.body.default, 'hello');
})); }));
it('can set null even if it has default value', async(async () => { test('can set null even if it has default value', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
nullableDefault: null, nullableDefault: null,
@@ -71,7 +71,7 @@ describe('API', () => {
assert.strictEqual(res.body.nullableDefault, null); assert.strictEqual(res.body.nullableDefault, null);
})); }));
it('cannot set undefined if it has default value', async(async () => { test('cannot set undefined if it has default value', async(async () => {
const res = await request('/test', { const res = await request('/test', {
required: true, required: true,
nullableDefault: undefined, nullableDefault: undefined,
+6 -6
View File
@@ -23,7 +23,7 @@ describe('Block', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('Block作成', async () => { test('Block作成', async () => {
const res = await request('/blocking/create', { const res = await request('/blocking/create', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@@ -31,14 +31,14 @@ describe('Block', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
it('ブロックされているユーザーをフォローできない', async () => { test('ブロックされているユーザーをフォローできない', async () => {
const res = await request('/following/create', { userId: alice.id }, bob); const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
}); });
it('ブロックされているユーザーにリアクションできない', async () => { test('ブロックされているユーザーにリアクションできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
@@ -47,7 +47,7 @@ describe('Block', () => {
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
}); });
it('ブロックされているユーザーに返信できない', async () => { test('ブロックされているユーザーに返信できない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
@@ -56,7 +56,7 @@ describe('Block', () => {
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
}); });
it('ブロックされているユーザーのノートをRenoteできない', async () => { test('ブロックされているユーザーのノートをRenoteできない', async () => {
const note = await post(alice, { text: 'hello' }); const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
@@ -69,7 +69,7 @@ describe('Block', () => {
// TODO: ユーザーリストから除外されるテスト // TODO: ユーザーリストから除外されるテスト
it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => { test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
const aliceNote = await post(alice); const aliceNote = await post(alice);
const bobNote = await post(bob); const bobNote = await post(bob);
const carolNote = await post(carol); const carolNote = await post(carol);
+71 -71
View File
@@ -22,7 +22,7 @@ describe('Endpoints', () => {
}); });
describe('signup', () => { describe('signup', () => {
it('不正なユーザー名でアカウントが作成できない', async () => { test('不正なユーザー名でアカウントが作成できない', async () => {
const res = await request('api/signup', { const res = await request('api/signup', {
username: 'test.', username: 'test.',
password: 'test', password: 'test',
@@ -30,7 +30,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('空のパスワードでアカウントが作成できない', async () => { test('空のパスワードでアカウントが作成できない', async () => {
const res = await request('api/signup', { const res = await request('api/signup', {
username: 'test', username: 'test',
password: '', password: '',
@@ -38,7 +38,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('正しくアカウントが作成できる', async () => { test('正しくアカウントが作成できる', async () => {
const me = { const me = {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
@@ -51,7 +51,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.username, me.username); assert.strictEqual(res.body.username, me.username);
}); });
it('同じユーザー名のアカウントは作成できない', async () => { test('同じユーザー名のアカウントは作成できない', async () => {
const res = await request('api/signup', { const res = await request('api/signup', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
@@ -62,7 +62,7 @@ describe('Endpoints', () => {
}); });
describe('signin', () => { describe('signin', () => {
it('間違ったパスワードでサインインできない', async () => { test('間違ったパスワードでサインインできない', async () => {
const res = await request('api/signin', { const res = await request('api/signin', {
username: 'test1', username: 'test1',
password: 'bar', password: 'bar',
@@ -71,7 +71,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 403); assert.strictEqual(res.status, 403);
}); });
it('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await request('api/signin', { const res = await request('api/signin', {
username: 'test1', username: 'test1',
password: { password: {
@@ -82,7 +82,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('正しい情報でサインインできる', async () => { test('正しい情報でサインインできる', async () => {
const res = await request('api/signin', { const res = await request('api/signin', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
@@ -93,7 +93,7 @@ describe('Endpoints', () => {
}); });
describe('i/update', () => { describe('i/update', () => {
it('アカウント設定を更新できる', async () => { test('アカウント設定を更新できる', async () => {
const myName = '大室櫻子'; const myName = '大室櫻子';
const myLocation = '七森中'; const myLocation = '七森中';
const myBirthday = '2000-09-07'; const myBirthday = '2000-09-07';
@@ -111,14 +111,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, myBirthday); assert.strictEqual(res.body.birthday, myBirthday);
}); });
it('名前を空白にできない', async () => { test('名前を空白にできない', async () => {
const res = await api('/i/update', { const res = await api('/i/update', {
name: ' ', name: ' ',
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('誕生日の設定を削除できる', async () => { test('誕生日の設定を削除できる', async () => {
await api('/i/update', { await api('/i/update', {
birthday: '2000-09-07', birthday: '2000-09-07',
}, alice); }, alice);
@@ -132,7 +132,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, null); assert.strictEqual(res.body.birthday, null);
}); });
it('不正な誕生日の形式で怒られる', async () => { test('不正な誕生日の形式で怒られる', async () => {
const res = await api('/i/update', { const res = await api('/i/update', {
birthday: '2000/09/07', birthday: '2000/09/07',
}, alice); }, alice);
@@ -141,7 +141,7 @@ describe('Endpoints', () => {
}); });
describe('users/show', () => { describe('users/show', () => {
it('ユーザーが取得できる', async () => { test('ユーザーが取得できる', async () => {
const res = await api('/users/show', { const res = await api('/users/show', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@@ -151,14 +151,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.id, alice.id); assert.strictEqual(res.body.id, alice.id);
}); });
it('ユーザーが存在しなかったら怒る', async () => { test('ユーザーが存在しなかったら怒る', async () => {
const res = await api('/users/show', { const res = await api('/users/show', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/users/show', { const res = await api('/users/show', {
userId: 'kyoppie', userId: 'kyoppie',
}); });
@@ -167,7 +167,7 @@ describe('Endpoints', () => {
}); });
describe('notes/show', () => { describe('notes/show', () => {
it('投稿が取得できる', async () => { test('投稿が取得できる', async () => {
const myPost = await post(alice, { const myPost = await post(alice, {
text: 'test', text: 'test',
}); });
@@ -182,14 +182,14 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.text, myPost.text); assert.strictEqual(res.body.text, myPost.text);
}); });
it('投稿が存在しなかったら怒る', async () => { test('投稿が存在しなかったら怒る', async () => {
const res = await api('/notes/show', { const res = await api('/notes/show', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
}); });
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/show', { const res = await api('/notes/show', {
noteId: 'kyoppie', noteId: 'kyoppie',
}); });
@@ -198,7 +198,7 @@ describe('Endpoints', () => {
}); });
describe('notes/reactions/create', () => { describe('notes/reactions/create', () => {
it('リアクションできる', async () => { test('リアクションできる', async () => {
const bobPost = await post(bob); const bobPost = await post(bob);
const alice = await signup({ username: 'alice' }); const alice = await signup({ username: 'alice' });
@@ -217,7 +217,7 @@ describe('Endpoints', () => {
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]);
}); });
it('自分の投稿にもリアクションできる', async () => { test('自分の投稿にもリアクションできる', async () => {
const myPost = await post(alice); const myPost = await post(alice);
const res = await api('/notes/reactions/create', { const res = await api('/notes/reactions/create', {
@@ -228,7 +228,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}); });
it('二重にリアクションできない', async () => { test('二重にリアクションできない', async () => {
const bobPost = await post(bob); const bobPost = await post(bob);
await api('/notes/reactions/create', { await api('/notes/reactions/create', {
@@ -244,7 +244,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しない投稿にはリアクションできない', async () => { test('存在しない投稿にはリアクションできない', async () => {
const res = await api('/notes/reactions/create', { const res = await api('/notes/reactions/create', {
noteId: '000000000000000000000000', noteId: '000000000000000000000000',
reaction: '🚀', reaction: '🚀',
@@ -253,13 +253,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/notes/reactions/create', {}, alice); const res = await api('/notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/notes/reactions/create', { const res = await api('/notes/reactions/create', {
noteId: 'kyoppie', noteId: 'kyoppie',
reaction: '🚀', reaction: '🚀',
@@ -270,7 +270,7 @@ describe('Endpoints', () => {
}); });
describe('following/create', () => { describe('following/create', () => {
it('フォローできる', async () => { test('フォローできる', async () => {
const res = await api('/following/create', { const res = await api('/following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@@ -278,7 +278,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
it('既にフォローしている場合は怒る', async () => { test('既にフォローしている場合は怒る', async () => {
const res = await api('/following/create', { const res = await api('/following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@@ -286,7 +286,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しないユーザーはフォローできない', async () => { test('存在しないユーザーはフォローできない', async () => {
const res = await api('/following/create', { const res = await api('/following/create', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@@ -294,7 +294,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('自分自身はフォローできない', async () => { test('自分自身はフォローできない', async () => {
const res = await api('/following/create', { const res = await api('/following/create', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@@ -302,13 +302,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/create', {}, alice); const res = await api('/following/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/create', { const res = await api('/following/create', {
userId: 'foo', userId: 'foo',
}, alice); }, alice);
@@ -318,7 +318,7 @@ describe('Endpoints', () => {
}); });
describe('following/delete', () => { describe('following/delete', () => {
it('フォロー解除できる', async () => { test('フォロー解除できる', async () => {
await api('/following/create', { await api('/following/create', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@@ -330,7 +330,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
it('フォローしていない場合は怒る', async () => { test('フォローしていない場合は怒る', async () => {
const res = await api('/following/delete', { const res = await api('/following/delete', {
userId: alice.id, userId: alice.id,
}, bob); }, bob);
@@ -338,7 +338,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しないユーザーはフォロー解除できない', async () => { test('存在しないユーザーはフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('/following/delete', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
}, alice); }, alice);
@@ -346,7 +346,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('自分自身はフォロー解除できない', async () => { test('自分自身はフォロー解除できない', async () => {
const res = await api('/following/delete', { const res = await api('/following/delete', {
userId: alice.id, userId: alice.id,
}, alice); }, alice);
@@ -354,13 +354,13 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('空のパラメータで怒られる', async () => { test('空のパラメータで怒られる', async () => {
const res = await api('/following/delete', {}, alice); const res = await api('/following/delete', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/following/delete', { const res = await api('/following/delete', {
userId: 'kyoppie', userId: 'kyoppie',
}, alice); }, alice);
@@ -371,7 +371,7 @@ describe('Endpoints', () => {
/* /*
describe('/i', () => { describe('/i', () => {
it('', async () => { test('', async () => {
}); });
}); });
*/ */
@@ -402,7 +402,7 @@ describe('API: Endpoints', () => {
}); });
describe('drive', () => { describe('drive', () => {
it('ドライブ情報を取得できる', async () => { test('ドライブ情報を取得できる', async () => {
await uploadFile({ await uploadFile({
userId: alice.id, userId: alice.id,
size: 256 size: 256
@@ -423,7 +423,7 @@ describe('API: Endpoints', () => {
}); });
describe('drive/files/create', () => { describe('drive/files/create', () => {
it('ファイルを作成できる', async () => { test('ファイルを作成できる', async () => {
const res = await uploadFile(alice); const res = await uploadFile(alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -431,7 +431,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, 'Lenna.png'); assert.strictEqual(res.body.name, 'Lenna.png');
})); }));
it('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {
const res = await assert.request(server) const res = await assert.request(server)
.post('/drive/files/create') .post('/drive/files/create')
.field('i', alice.token) .field('i', alice.token)
@@ -443,13 +443,13 @@ describe('API: Endpoints', () => {
expect(res.body).have.property('name').eql('Belmond.png'); expect(res.body).have.property('name').eql('Belmond.png');
})); }));
it('ファイル無しで怒られる', async () => { test('ファイル無しで怒られる', async () => {
const res = await api('/drive/files/create', {}, alice); const res = await api('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('SVGファイルを作成できる', async () => { test('SVGファイルを作成できる', async () => {
const res = await uploadFile(alice, __dirname + '/resources/image.svg'); const res = await uploadFile(alice, __dirname + '/resources/image.svg');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
@@ -460,7 +460,7 @@ describe('API: Endpoints', () => {
}); });
describe('drive/files/update', () => { describe('drive/files/update', () => {
it('名前を更新できる', async () => { test('名前を更新できる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const newName = 'いちごパスタ.png'; const newName = 'いちごパスタ.png';
@@ -474,7 +474,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, newName); assert.strictEqual(res.body.name, newName);
})); }));
it('他人のファイルは更新できない', async () => { test('他人のファイルは更新できない', async () => {
const file = await uploadFile(bob); const file = await uploadFile(bob);
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
@@ -485,7 +485,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
@@ -501,7 +501,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.folderId, folder.id); assert.strictEqual(res.body.folderId, folder.id);
})); }));
it('親フォルダを無しにできる', async () => { test('親フォルダを無しにできる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
@@ -523,7 +523,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.folderId, null); assert.strictEqual(res.body.folderId, null);
})); }));
it('他人のフォルダには入れられない', async () => { test('他人のフォルダには入れられない', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
@@ -537,7 +537,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('存在しないフォルダで怒られる', async () => { test('存在しないフォルダで怒られる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
@@ -548,7 +548,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const file = await uploadFile(alice); const file = await uploadFile(alice);
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
@@ -559,7 +559,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('ファイルが存在しなかったら怒る', async () => { test('ファイルが存在しなかったら怒る', async () => {
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: '000000000000000000000000', fileId: '000000000000000000000000',
name: 'いちごパスタ.png' name: 'いちごパスタ.png'
@@ -568,7 +568,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: 'kyoppie', fileId: 'kyoppie',
name: 'いちごパスタ.png' name: 'いちごパスタ.png'
@@ -579,7 +579,7 @@ describe('API: Endpoints', () => {
}); });
describe('drive/folders/create', () => { describe('drive/folders/create', () => {
it('フォルダを作成できる', async () => { test('フォルダを作成できる', async () => {
const res = await api('/drive/folders/create', { const res = await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice); }, alice);
@@ -591,7 +591,7 @@ describe('API: Endpoints', () => {
}); });
describe('drive/folders/update', () => { describe('drive/folders/update', () => {
it('名前を更新できる', async () => { test('名前を更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -606,7 +606,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.name, 'new name'); assert.strictEqual(res.body.name, 'new name');
})); }));
it('他人のフォルダを更新できない', async () => { test('他人のフォルダを更新できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, bob)).body; }, bob)).body;
@@ -619,7 +619,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('親フォルダを更新できる', async () => { test('親フォルダを更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -637,7 +637,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.parentId, parentFolder.id); assert.strictEqual(res.body.parentId, parentFolder.id);
})); }));
it('親フォルダを無しに更新できる', async () => { test('親フォルダを無しに更新できる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -659,7 +659,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.parentId, null); assert.strictEqual(res.body.parentId, null);
})); }));
it('他人のフォルダを親フォルダに設定できない', async () => { test('他人のフォルダを親フォルダに設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -675,7 +675,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('フォルダが循環するような構造にできない', async () => { test('フォルダが循環するような構造にできない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -695,7 +695,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('フォルダが循環するような構造にできない(再帰的)', async () => { test('フォルダが循環するような構造にできない(再帰的)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -722,7 +722,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('フォルダが循環するような構造にできない(自身)', async () => { test('フォルダが循環するような構造にできない(自身)', async () => {
const folderA = (await api('/drive/folders/create', { const folderA = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -735,7 +735,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('存在しない親フォルダを設定できない', async () => { test('存在しない親フォルダを設定できない', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -748,7 +748,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('不正な親フォルダIDで怒られる', async () => { test('不正な親フォルダIDで怒られる', async () => {
const folder = (await api('/drive/folders/create', { const folder = (await api('/drive/folders/create', {
name: 'test' name: 'test'
}, alice)).body; }, alice)).body;
@@ -761,7 +761,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('存在しないフォルダを更新できない', async () => { test('存在しないフォルダを更新できない', async () => {
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: '000000000000000000000000' folderId: '000000000000000000000000'
}, alice); }, alice);
@@ -769,7 +769,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('不正なフォルダIDで怒られる', async () => { test('不正なフォルダIDで怒られる', async () => {
const res = await api('/drive/folders/update', { const res = await api('/drive/folders/update', {
folderId: 'foo' folderId: 'foo'
}, alice); }, alice);
@@ -779,7 +779,7 @@ describe('API: Endpoints', () => {
}); });
describe('messaging/messages/create', () => { describe('messaging/messages/create', () => {
it('メッセージを送信できる', async () => { test('メッセージを送信できる', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: bob.id, userId: bob.id,
text: 'test' text: 'test'
@@ -790,7 +790,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.body.text, 'test'); assert.strictEqual(res.body.text, 'test');
})); }));
it('自分自身にはメッセージを送信できない', async () => { test('自分自身にはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: alice.id, userId: alice.id,
text: 'Yo' text: 'Yo'
@@ -799,7 +799,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('存在しないユーザーにはメッセージを送信できない', async () => { test('存在しないユーザーにはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: '000000000000000000000000', userId: '000000000000000000000000',
text: 'test' text: 'test'
@@ -808,7 +808,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('不正なユーザーIDで怒られる', async () => { test('不正なユーザーIDで怒られる', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: 'foo', userId: 'foo',
text: 'test' text: 'test'
@@ -817,7 +817,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('テキストが無くて怒られる', async () => { test('テキストが無くて怒られる', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: bob.id userId: bob.id
}, alice); }, alice);
@@ -825,7 +825,7 @@ describe('API: Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
})); }));
it('文字数オーバーで怒られる', async () => { test('文字数オーバーで怒られる', async () => {
const res = await api('/messaging/messages/create', { const res = await api('/messaging/messages/create', {
userId: bob.id, userId: bob.id,
text: '!'.repeat(1001) text: '!'.repeat(1001)
@@ -836,7 +836,7 @@ describe('API: Endpoints', () => {
}); });
describe('notes/replies', () => { describe('notes/replies', () => {
it('自分に閲覧権限のない投稿は含まれない', async () => { test('自分に閲覧権限のない投稿は含まれない', async () => {
const alicePost = await post(alice, { const alicePost = await post(alice, {
text: 'foo' text: 'foo'
}); });
@@ -859,7 +859,7 @@ describe('API: Endpoints', () => {
}); });
describe('notes/timeline', () => { describe('notes/timeline', () => {
it('フォロワー限定投稿が含まれる', async () => { test('フォロワー限定投稿が含まれる', async () => {
await api('/following/create', { await api('/following/create', {
userId: alice.id userId: alice.id
}, bob); }, bob);
+25 -25
View File
@@ -35,38 +35,38 @@ describe('Fetch resource', () => {
}); });
describe('Common', () => { describe('Common', () => {
it('meta', async () => { test('meta', async () => {
const res = await request('/meta', { const res = await request('/meta', {
}); });
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
it('GET root', async () => { test('GET root', async () => {
const res = await simpleGet('/'); const res = await simpleGet('/');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
}); });
it('GET docs', async () => { test('GET docs', async () => {
const res = await simpleGet('/docs/ja-JP/about'); const res = await simpleGet('/docs/ja-JP/about');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
}); });
it('GET api-doc', async () => { test('GET api-doc', async () => {
const res = await simpleGet('/api-doc'); const res = await simpleGet('/api-doc');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
}); });
it('GET api.json', async () => { test('GET api.json', async () => {
const res = await simpleGet('/api.json'); const res = await simpleGet('/api.json');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON); assert.strictEqual(res.type, JSON);
}); });
it('Validate api.json', async () => { test('Validate api.json', async () => {
const config = await openapi.loadConfig(); const config = await openapi.loadConfig();
const result = await openapi.bundle({ const result = await openapi.bundle({
config, config,
@@ -80,25 +80,25 @@ describe('Fetch resource', () => {
assert.strictEqual(result.problems.length, 0); assert.strictEqual(result.problems.length, 0);
}); });
it('GET favicon.ico', async () => { test('GET favicon.ico', async () => {
const res = await simpleGet('/favicon.ico'); const res = await simpleGet('/favicon.ico');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/x-icon'); assert.strictEqual(res.type, 'image/x-icon');
}); });
it('GET apple-touch-icon.png', async () => { test('GET apple-touch-icon.png', async () => {
const res = await simpleGet('/apple-touch-icon.png'); const res = await simpleGet('/apple-touch-icon.png');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/png'); assert.strictEqual(res.type, 'image/png');
}); });
it('GET twemoji svg', async () => { test('GET twemoji svg', async () => {
const res = await simpleGet('/twemoji/2764.svg'); const res = await simpleGet('/twemoji/2764.svg');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml'); assert.strictEqual(res.type, 'image/svg+xml');
}); });
it('GET twemoji svg with hyphen', async () => { test('GET twemoji svg with hyphen', async () => {
const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg');
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml'); assert.strictEqual(res.type, 'image/svg+xml');
@@ -106,25 +106,25 @@ describe('Fetch resource', () => {
}); });
describe('/@:username', () => { describe('/@:username', () => {
it('Only AP => AP', async () => { test('Only AP => AP', async () => {
const res = await simpleGet(`/@${alice.username}`, ONLY_AP); const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer AP => AP', async () => { test('Prefer AP => AP', async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_AP); const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer HTML => HTML', async () => { test('Prefer HTML => HTML', async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
}); });
it('Unspecified => HTML', async () => { test('Unspecified => HTML', async () => {
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
@@ -132,25 +132,25 @@ describe('Fetch resource', () => {
}); });
describe('/users/:id', () => { describe('/users/:id', () => {
it('Only AP => AP', async () => { test('Only AP => AP', async () => {
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer AP => AP', async () => { test('Prefer AP => AP', async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer HTML => Redirect to /@:username', async () => { test('Prefer HTML => Redirect to /@:username', async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
assert.strictEqual(res.status, 302); assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`); assert.strictEqual(res.location, `/@${alice.username}`);
}); });
it('Undecided => HTML', async () => { test('Undecided => HTML', async () => {
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 302); assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`); assert.strictEqual(res.location, `/@${alice.username}`);
@@ -158,25 +158,25 @@ describe('Fetch resource', () => {
}); });
describe('/notes/:id', () => { describe('/notes/:id', () => {
it('Only AP => AP', async () => { test('Only AP => AP', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer AP => AP', async () => { test('Prefer AP => AP', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP); assert.strictEqual(res.type, AP);
}); });
it('Prefer HTML => HTML', async () => { test('Prefer HTML => HTML', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
}); });
it('Unspecified => HTML', async () => { test('Unspecified => HTML', async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML); assert.strictEqual(res.type, HTML);
@@ -184,19 +184,19 @@ describe('Fetch resource', () => {
}); });
describe('Feeds', () => { describe('Feeds', () => {
it('RSS', async () => { test('RSS', async () => {
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8');
}); });
it('ATOM', async () => { test('ATOM', async () => {
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8');
}); });
it('JSON', async () => { test('JSON', async () => {
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/json; charset=utf-8'); assert.strictEqual(res.type, 'application/json; charset=utf-8');
+7 -7
View File
@@ -22,7 +22,7 @@ describe('FF visibility', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'public', ffVisibility: 'public',
}, alice); }, alice);
@@ -40,7 +40,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
}); });
it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'followers', ffVisibility: 'followers',
}, alice); }, alice);
@@ -58,7 +58,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
}); });
it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'followers', ffVisibility: 'followers',
}, alice); }, alice);
@@ -74,7 +74,7 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400); assert.strictEqual(followersRes.status, 400);
}); });
it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'followers', ffVisibility: 'followers',
}, alice); }, alice);
@@ -96,7 +96,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
}); });
it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'private', ffVisibility: 'private',
}, alice); }, alice);
@@ -114,7 +114,7 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(Array.isArray(followersRes.body), true);
}); });
it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'private', ffVisibility: 'private',
}, alice); }, alice);
@@ -131,7 +131,7 @@ describe('FF visibility', () => {
}); });
describe('AP', () => { describe('AP', () => {
it('ffVisibility が public 以外ならばAPからは取得できない', async () => { test('ffVisibility が public 以外ならばAPからは取得できない', async () => {
{ {
await request('/i/update', { await request('/i/update', {
ffVisibility: 'public', ffVisibility: 'public',
+8 -8
View File
@@ -23,7 +23,7 @@ describe('Mute', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await request('/mute/create', { const res = await request('/mute/create', {
userId: carol.id, userId: carol.id,
}, alice); }, alice);
@@ -31,7 +31,7 @@ describe('Mute', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}); });
it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice hi' }); const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' }); const carolNote = await post(carol, { text: '@alice hi' });
@@ -43,7 +43,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
}); });
it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await request('/i/read-all-unread-notes', {}, alice); await request('/i/read-all-unread-notes', {}, alice);
@@ -55,7 +55,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
}); });
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await request('/i/read-all-unread-notes', {}, alice); await request('/i/read-all-unread-notes', {}, alice);
@@ -64,7 +64,7 @@ describe('Mute', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット // 状態リセット
await request('/i/read-all-unread-notes', {}, alice); await request('/i/read-all-unread-notes', {}, alice);
await request('/notifications/mark-all-as-read', {}, alice); await request('/notifications/mark-all-as-read', {}, alice);
@@ -75,7 +75,7 @@ describe('Mute', () => {
}); });
describe('Timeline', () => { describe('Timeline', () => {
it('タイムラインにミュートしているユーザーの投稿が含まれない', async () => { test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
const aliceNote = await post(alice); const aliceNote = await post(alice);
const bobNote = await post(bob); const bobNote = await post(bob);
const carolNote = await post(carol); const carolNote = await post(carol);
@@ -89,7 +89,7 @@ describe('Mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
}); });
it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
const aliceNote = await post(alice); const aliceNote = await post(alice);
const carolNote = await post(carol); const carolNote = await post(carol);
const bobNote = await post(bob, { const bobNote = await post(bob, {
@@ -107,7 +107,7 @@ describe('Mute', () => {
}); });
describe('Notification', () => { describe('Notification', () => {
it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
const aliceNote = await post(alice); const aliceNote = await post(alice);
await react(bob, aliceNote, 'like'); await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like'); await react(carol, aliceNote, 'like');
+25 -25
View File
@@ -24,7 +24,7 @@ describe('Note', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('投稿できる', async () => { test('投稿できる', async () => {
const post = { const post = {
text: 'test', text: 'test',
}; };
@@ -36,7 +36,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.text, post.text); assert.strictEqual(res.body.createdNote.text, post.text);
}); });
it('ファイルを添付できる', 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/Lenna.jpg');
const res = await request('/notes/create', { const res = await request('/notes/create', {
@@ -48,7 +48,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
}, 1000 * 10); }, 1000 * 10);
it('他人のファイルは無視', 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/Lenna.jpg');
const res = await request('/notes/create', { const res = await request('/notes/create', {
@@ -61,7 +61,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []); assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}, 1000 * 10); }, 1000 * 10);
it('存在しないファイルは無視', async () => { test('存在しないファイルは無視', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
text: 'test', text: 'test',
fileIds: ['000000000000000000000000'], fileIds: ['000000000000000000000000'],
@@ -72,7 +72,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []); assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}); });
it('不正なファイルIDは無視', async () => { test('不正なファイルIDは無視', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
fileIds: ['kyoppie'], fileIds: ['kyoppie'],
}, alice); }, alice);
@@ -81,7 +81,7 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []); assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}); });
it('返信できる', async () => { test('返信できる', async () => {
const bobPost = await post(bob, { const bobPost = await post(bob, {
text: 'foo', text: 'foo',
}); });
@@ -100,7 +100,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
}); });
it('renoteできる', async () => { test('renoteできる', async () => {
const bobPost = await post(bob, { const bobPost = await post(bob, {
text: 'test', text: 'test',
}); });
@@ -117,7 +117,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}); });
it('引用renoteできる', async () => { test('引用renoteできる', async () => {
const bobPost = await post(bob, { const bobPost = await post(bob, {
text: 'test', text: 'test',
}); });
@@ -136,7 +136,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}); });
it('文字数ぎりぎりで怒られない', async () => { test('文字数ぎりぎりで怒られない', async () => {
const post = { const post = {
text: '!'.repeat(3000), text: '!'.repeat(3000),
}; };
@@ -144,7 +144,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
}); });
it('文字数オーバーで怒られる', async () => { test('文字数オーバーで怒られる', async () => {
const post = { const post = {
text: '!'.repeat(3001), text: '!'.repeat(3001),
}; };
@@ -152,7 +152,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しないリプライ先で怒られる', async () => { test('存在しないリプライ先で怒られる', async () => {
const post = { const post = {
text: 'test', text: 'test',
replyId: '000000000000000000000000', replyId: '000000000000000000000000',
@@ -161,7 +161,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しないrenote対象で怒られる', async () => { test('存在しないrenote対象で怒られる', async () => {
const post = { const post = {
renoteId: '000000000000000000000000', renoteId: '000000000000000000000000',
}; };
@@ -169,7 +169,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('不正なリプライ先IDで怒られる', async () => { test('不正なリプライ先IDで怒られる', async () => {
const post = { const post = {
text: 'test', text: 'test',
replyId: 'foo', replyId: 'foo',
@@ -178,7 +178,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('不正なrenote対象IDで怒られる', async () => { test('不正なrenote対象IDで怒られる', async () => {
const post = { const post = {
renoteId: 'foo', renoteId: 'foo',
}; };
@@ -186,7 +186,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('存在しないユーザーにメンションできる', async () => { test('存在しないユーザーにメンションできる', async () => {
const post = { const post = {
text: '@ghost yo', text: '@ghost yo',
}; };
@@ -198,7 +198,7 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.text, post.text); assert.strictEqual(res.body.createdNote.text, post.text);
}); });
it('同じユーザーに複数メンションしても内部的にまとめられる', async () => { test('同じユーザーに複数メンションしても内部的にまとめられる', async () => {
const post = { const post = {
text: '@bob @bob @bob yo', text: '@bob @bob @bob yo',
}; };
@@ -214,7 +214,7 @@ describe('Note', () => {
}); });
describe('notes/create', () => { describe('notes/create', () => {
it('投票を添付できる', async () => { test('投票を添付できる', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
text: 'test', text: 'test',
poll: { poll: {
@@ -227,14 +227,14 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.poll != null, true); assert.strictEqual(res.body.createdNote.poll != null, true);
}); });
it('投票の選択肢が無くて怒られる', async () => { test('投票の選択肢が無くて怒られる', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
poll: {}, poll: {},
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('投票の選択肢が無くて怒られる (空の配列)', async () => { test('投票の選択肢が無くて怒られる (空の配列)', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
poll: { poll: {
choices: [], choices: [],
@@ -243,7 +243,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('投票の選択肢が1つで怒られる', async () => { test('投票の選択肢が1つで怒られる', async () => {
const res = await request('/notes/create', { const res = await request('/notes/create', {
poll: { poll: {
choices: ['Strawberry Pasta'], choices: ['Strawberry Pasta'],
@@ -252,7 +252,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('投票できる', async () => { test('投票できる', async () => {
const { body } = await request('/notes/create', { const { body } = await request('/notes/create', {
text: 'test', text: 'test',
poll: { poll: {
@@ -268,7 +268,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}); });
it('複数投票できない', async () => { test('複数投票できない', async () => {
const { body } = await request('/notes/create', { const { body } = await request('/notes/create', {
text: 'test', text: 'test',
poll: { poll: {
@@ -289,7 +289,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
it('許可されている場合は複数投票できる', async () => { test('許可されている場合は複数投票できる', async () => {
const { body } = await request('/notes/create', { const { body } = await request('/notes/create', {
text: 'test', text: 'test',
poll: { poll: {
@@ -316,7 +316,7 @@ describe('Note', () => {
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
}); });
it('締め切られている場合は投票できない', async () => { test('締め切られている場合は投票できない', async () => {
const { body } = await request('/notes/create', { const { body } = await request('/notes/create', {
text: 'test', text: 'test',
poll: { poll: {
@@ -337,7 +337,7 @@ describe('Note', () => {
}); });
describe('notes/delete', () => { describe('notes/delete', () => {
it('delete a reply', async () => { test('delete a reply', async () => {
const mainNoteRes = await api('notes/create', { const mainNoteRes = await api('notes/create', {
text: 'main post', text: 'main post',
}, alice); }, alice);
+33 -33
View File
@@ -78,7 +78,7 @@ describe('Streaming', () => {
}); });
describe('Events', () => { describe('Events', () => {
it('mention event', async () => { test('mention event', async () => {
const fired = await waitFire( const fired = await waitFire(
kyoko, 'main', // kyoko:main kyoko, 'main', // kyoko:main
() => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko
@@ -88,7 +88,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('renote event', async () => { test('renote event', async () => {
const fired = await waitFire( const fired = await waitFire(
kyoko, 'main', // kyoko:main kyoko, 'main', // kyoko:main
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
@@ -100,7 +100,7 @@ describe('Streaming', () => {
}); });
describe('Home Timeline', () => { describe('Home Timeline', () => {
it('自分の投稿が流れる', async () => { test('自分の投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'homeTimeline', // ayano:Home ayano, 'homeTimeline', // ayano:Home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -110,7 +110,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしているユーザーの投稿が流れる', async () => { test('フォローしているユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts
@@ -120,7 +120,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないユーザーの投稿は流れない', async () => { test('フォローしていないユーザーの投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
kyoko, 'homeTimeline', // kyoko:home kyoko, 'homeTimeline', // kyoko:home
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -130,7 +130,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしているユーザーのダイレクト投稿が流れる', async () => { test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano
@@ -140,7 +140,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => { test('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose
@@ -152,7 +152,7 @@ describe('Streaming', () => {
}); // Home }); // Home
describe('Local Timeline', () => { describe('Local Timeline', () => {
it('自分の投稿が流れる', async () => { test('自分の投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -162,7 +162,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないローカルユーザーの投稿が流れる', async () => { test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -172,7 +172,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('リモートユーザーの投稿は流れない', async () => { test('リモートユーザーの投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -182,7 +182,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { test('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo' }, akari), // akari posts () => api('notes/create', { text: 'foo' }, akari), // akari posts
@@ -192,7 +192,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('ホーム指定の投稿は流れない', async () => { test('ホーム指定の投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts
@@ -202,7 +202,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { test('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano
@@ -212,7 +212,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'localTimeline', // ayano:Local ayano, 'localTimeline', // ayano:Local
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
@@ -224,7 +224,7 @@ describe('Streaming', () => {
}); });
describe('Hybrid Timeline', () => { describe('Hybrid Timeline', () => {
it('自分の投稿が流れる', async () => { test('自分の投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, ayano), // ayano posts () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
@@ -234,7 +234,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないローカルユーザーの投稿が流れる', async () => { test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -244,7 +244,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしているリモートユーザーの投稿が流れる', async () => { test('フォローしているリモートユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, akari), // akari posts () => api('notes/create', { text: 'foo' }, akari), // akari posts
@@ -254,7 +254,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないリモートユーザーの投稿は流れない', async () => { test('フォローしていないリモートユーザーの投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -264,7 +264,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしているユーザーのダイレクト投稿が流れる', async () => { test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
@@ -274,7 +274,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしているユーザーのホーム投稿が流れる', async () => { test('フォローしているユーザーのホーム投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
@@ -284,7 +284,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'home' }, chitose), () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
@@ -294,7 +294,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, false); assert.strictEqual(fired, false);
}); });
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
@@ -306,7 +306,7 @@ describe('Streaming', () => {
}); });
describe('Global Timeline', () => { describe('Global Timeline', () => {
it('フォローしていないローカルユーザーの投稿が流れる', async () => { test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chitose), // chitose posts () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
@@ -316,7 +316,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('フォローしていないリモートユーザーの投稿が流れる', async () => { test('フォローしていないリモートユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
@@ -326,7 +326,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('ホーム投稿は流れない', async () => { test('ホーム投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
ayano, 'globalTimeline', // ayano:Global ayano, 'globalTimeline', // ayano:Global
() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts
@@ -338,7 +338,7 @@ describe('Streaming', () => {
}); });
describe('UserList Timeline', () => { describe('UserList Timeline', () => {
it('リストに入れているユーザーの投稿が流れる', async () => { test('リストに入れているユーザーの投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
chitose, 'userList', chitose, 'userList',
() => api('notes/create', { text: 'foo' }, ayano), () => api('notes/create', { text: 'foo' }, ayano),
@@ -349,7 +349,7 @@ describe('Streaming', () => {
assert.strictEqual(fired, true); assert.strictEqual(fired, true);
}); });
it('リストに入れていないユーザーの投稿は流れない', async () => { test('リストに入れていないユーザーの投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
chitose, 'userList', chitose, 'userList',
() => api('notes/create', { text: 'foo' }, chinatsu), () => api('notes/create', { text: 'foo' }, chinatsu),
@@ -361,7 +361,7 @@ describe('Streaming', () => {
}); });
// #4471 // #4471
it('リストに入れているユーザーのダイレクト投稿が流れる', async () => { test('リストに入れているユーザーのダイレクト投稿が流れる', async () => {
const fired = await waitFire( const fired = await waitFire(
chitose, 'userList', chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano), () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
@@ -373,7 +373,7 @@ describe('Streaming', () => {
}); });
// #4335 // #4335
it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => { test('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => {
const fired = await waitFire( const fired = await waitFire(
chitose, 'userList', chitose, 'userList',
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
@@ -386,7 +386,7 @@ describe('Streaming', () => {
}); });
describe('Hashtag Timeline', () => { describe('Hashtag Timeline', () => {
it('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => { test('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
if (type === 'note') { if (type === 'note') {
assert.deepStrictEqual(body.text, '#foo'); assert.deepStrictEqual(body.text, '#foo');
@@ -404,7 +404,7 @@ describe('Streaming', () => {
}); });
})); }));
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => { test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
let fooCount = 0; let fooCount = 0;
let barCount = 0; let barCount = 0;
let fooBarCount = 0; let fooBarCount = 0;
@@ -442,7 +442,7 @@ describe('Streaming', () => {
}, 3000); }, 3000);
})); }));
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => { test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
let fooCount = 0; let fooCount = 0;
let barCount = 0; let barCount = 0;
let fooBarCount = 0; let fooBarCount = 0;
@@ -488,7 +488,7 @@ describe('Streaming', () => {
}, 3000); }, 3000);
})); }));
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => { test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
let fooCount = 0; let fooCount = 0;
let barCount = 0; let barCount = 0;
let fooBarCount = 0; let fooBarCount = 0;
+4 -4
View File
@@ -22,7 +22,7 @@ describe('Note thread mute', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
@@ -40,7 +40,7 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
}); });
it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
// 状態リセット // 状態リセット
await request('/i/read-all-unread-notes', {}, alice); await request('/i/read-all-unread-notes', {}, alice);
@@ -56,7 +56,7 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.hasUnreadMentions, false); assert.strictEqual(res.body.hasUnreadMentions, false);
}); });
it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
// 状態リセット // 状態リセット
await request('/i/read-all-unread-notes', {}, alice); await request('/i/read-all-unread-notes', {}, alice);
@@ -82,7 +82,7 @@ describe('Note thread mute', () => {
}, 5000); }, 5000);
})); }));
it('i/notifications にミュートしているスレッドの通知が含まれない', async () => { test('i/notifications にミュートしているスレッドの通知が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
+2 -2
View File
@@ -32,7 +32,7 @@ describe('users/notes', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('ファイルタイプ指定 (jpg)', async () => { test('ファイルタイプ指定 (jpg)', async () => {
const res = await request('/users/notes', { const res = await request('/users/notes', {
userId: alice.id, userId: alice.id,
fileType: ['image/jpeg'], fileType: ['image/jpeg'],
@@ -45,7 +45,7 @@ describe('users/notes', () => {
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
}); });
it('ファイルタイプ指定 (jpg or png)', async () => { test('ファイルタイプ指定 (jpg or png)', async () => {
const res = await request('/users/notes', { const res = await request('/users/notes', {
userId: alice.id, userId: alice.id,
fileType: ['image/jpeg', 'image/png'], fileType: ['image/jpeg', 'image/png'],
+3 -3
View File
@@ -2,17 +2,17 @@ import * as assert from 'assert';
import { just, nothing } from '../../src/misc/prelude/maybe.js'; import { just, nothing } from '../../src/misc/prelude/maybe.js';
describe('just', () => { describe('just', () => {
it('has a value', () => { test('has a value', () => {
assert.deepStrictEqual(just(3).isJust(), true); assert.deepStrictEqual(just(3).isJust(), true);
}); });
it('has the inverse called get', () => { test('has the inverse called get', () => {
assert.deepStrictEqual(just(3).get(), 3); assert.deepStrictEqual(just(3).get(), 3);
}); });
}); });
describe('nothing', () => { describe('nothing', () => {
it('has no value', () => { test('has no value', () => {
assert.deepStrictEqual(nothing().isJust(), false); assert.deepStrictEqual(nothing().isJust(), false);
}); });
}); });
+1 -1
View File
@@ -2,7 +2,7 @@ import * as assert from 'assert';
import { query } from '../../src/misc/prelude/url.js'; import { query } from '../../src/misc/prelude/url.js';
describe('url', () => { describe('url', () => {
it('query', () => { test('query', () => {
const s = query({ const s = query({
foo: 'ふぅ', foo: 'ふぅ',
bar: 'b a r', bar: 'b a r',
+3 -3
View File
@@ -27,7 +27,7 @@ describe('ActivityPub', () => {
content: 'あ', content: 'あ',
}; };
it('Minimum Actor', async () => { test('Minimum Actor', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js'); const { MockResolver } = await import('../misc/mock-resolver.js');
const { createPerson } = await import('../../src/activitypub/models/person.js'); const { createPerson } = await import('../../src/activitypub/models/person.js');
@@ -41,7 +41,7 @@ describe('ActivityPub', () => {
assert.deepStrictEqual(user.inbox, actor.inbox); assert.deepStrictEqual(user.inbox, actor.inbox);
}); });
it('Minimum Note', async () => { test('Minimum Note', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js'); const { MockResolver } = await import('../misc/mock-resolver.js');
const { createNote } = await import('../../src/activitypub/models/note.js'); const { createNote } = await import('../../src/activitypub/models/note.js');
@@ -74,7 +74,7 @@ describe('ActivityPub', () => {
outbox: `${actorId}/outbox`, outbox: `${actorId}/outbox`,
}; };
it('Actor', async () => { test('Actor', async () => {
const { MockResolver } = await import('../misc/mock-resolver.js'); const { MockResolver } = await import('../misc/mock-resolver.js');
const { createPerson } = await import('../../src/activitypub/models/person.js'); const { createPerson } = await import('../../src/activitypub/models/person.js');
+2 -2
View File
@@ -19,7 +19,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
}; };
describe('ap-request', () => { describe('ap-request', () => {
it('createSignedPost with verify', async () => { test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair(); const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/inbox'; const url = 'https://example.com/inbox';
@@ -37,7 +37,7 @@ describe('ap-request', () => {
assert.deepStrictEqual(result, true); assert.deepStrictEqual(result, true);
}); });
it('createSignedGet with verify', async () => { test('createSignedGet with verify', async () => {
const keypair = await genRsaKeyPair(); const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/outbox'; const url = 'https://example.com/outbox';
@@ -4,7 +4,7 @@ import { parse } from 'mfm-js';
import { extractMentions } from '../../src/misc/extract-mentions.js'; import { extractMentions } from '../../src/misc/extract-mentions.js';
describe('Extract mentions', () => { describe('Extract mentions', () => {
it('simple', () => { test('simple', () => {
const ast = parse('@foo @bar @baz')!; const ast = parse('@foo @bar @baz')!;
const mentions = extractMentions(ast); const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{ assert.deepStrictEqual(mentions, [{
@@ -22,7 +22,7 @@ describe('Extract mentions', () => {
}]); }]);
}); });
it('nested', () => { test('nested', () => {
const ast = parse('@foo **@bar** @baz')!; const ast = parse('@foo **@bar** @baz')!;
const mentions = extractMentions(ast); const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{ assert.deepStrictEqual(mentions, [{
+19 -19
View File
@@ -5,13 +5,13 @@ import { toHtml } from '../../src/mfm/to-html.js';
import { fromHtml } from '../../src/mfm/from-html.js'; import { fromHtml } from '../../src/mfm/from-html.js';
describe('toHtml', () => { describe('toHtml', () => {
it('br', () => { test('br', () => {
const input = 'foo\nbar\nbaz'; const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>'; const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output); assert.equal(toHtml(mfm.parse(input)), output);
}); });
it('br alt', () => { test('br alt', () => {
const input = 'foo\r\nbar\rbaz'; const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>'; const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output); assert.equal(toHtml(mfm.parse(input)), output);
@@ -19,71 +19,71 @@ describe('toHtml', () => {
}); });
describe('fromHtml', () => { describe('fromHtml', () => {
it('p', () => { test('p', () => {
assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb'); assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb');
}); });
it('block element', () => { test('block element', () => {
assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb'); assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb');
}); });
it('inline element', () => { test('inline element', () => {
assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb'); assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb');
}); });
it('block code', () => { test('block code', () => {
assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```'); assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```');
}); });
it('inline code', () => { test('inline code', () => {
assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`'); assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`');
}); });
it('quote', () => { test('quote', () => {
assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b'); assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b');
}); });
it('br', () => { test('br', () => {
assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd'); assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd');
}); });
it('link with different text', () => { test('link with different text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">c</a> d</p>'), 'a [c](https://example.com/b) d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">c</a> d</p>'), 'a [c](https://example.com/b) d');
}); });
it('link with different text, but not encoded', () => { test('link with different text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">c</a> d</p>'), 'a [c](<https://example.com/ä>) d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">c</a> d</p>'), 'a [c](<https://example.com/ä>) d');
}); });
it('link with same text', () => { test('link with same text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">https://example.com/b</a> d</p>'), 'a https://example.com/b d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">https://example.com/b</a> d</p>'), 'a https://example.com/b d');
}); });
it('link with same text, but not encoded', () => { test('link with same text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">https://example.com/ä</a> d</p>'), 'a <https://example.com/ä> d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">https://example.com/ä</a> d</p>'), 'a <https://example.com/ä> d');
}); });
it('link with no url', () => { test('link with no url', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="b">c</a> d</p>'), 'a [c](b) d'); assert.deepStrictEqual(fromHtml('<p>a <a href="b">c</a> d</p>'), 'a [c](b) d');
}); });
it('link without href', () => { test('link without href', () => {
assert.deepStrictEqual(fromHtml('<p>a <a>c</a> d</p>'), 'a c d'); assert.deepStrictEqual(fromHtml('<p>a <a>c</a> d</p>'), 'a c d');
}); });
it('link without text', () => { test('link without text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b"></a> d</p>'), 'a https://example.com/b d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b"></a> d</p>'), 'a https://example.com/b d');
}); });
it('link without both', () => { test('link without both', () => {
assert.deepStrictEqual(fromHtml('<p>a <a></a> d</p>'), 'a d'); assert.deepStrictEqual(fromHtml('<p>a <a></a> d</p>'), 'a d');
}); });
it('mention', () => { test('mention', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
}); });
it('hashtag', () => { test('hashtag', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d'); assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
}); });
}); });
+19 -19
View File
@@ -4,79 +4,79 @@ import * as assert from 'assert';
import { toDbReaction } from '../src/misc/reaction-lib.js'; import { toDbReaction } from '../src/misc/reaction-lib.js';
describe('toDbReaction', async () => { describe('toDbReaction', async () => {
it('既存の文字列リアクションはそのまま', async () => { test('既存の文字列リアクションはそのまま', async () => {
assert.strictEqual(await toDbReaction('like'), 'like'); assert.strictEqual(await toDbReaction('like'), 'like');
}); });
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { test('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
assert.strictEqual(await toDbReaction('🍮'), '🍮'); assert.strictEqual(await toDbReaction('🍮'), '🍮');
}); });
it('プリン以外の既存のリアクションは文字列化する like', async () => { test('プリン以外の既存のリアクションは文字列化する like', async () => {
assert.strictEqual(await toDbReaction('👍'), 'like'); assert.strictEqual(await toDbReaction('👍'), 'like');
}); });
it('プリン以外の既存のリアクションは文字列化する love', async () => { test('プリン以外の既存のリアクションは文字列化する love', async () => {
assert.strictEqual(await toDbReaction('❤️'), 'love'); assert.strictEqual(await toDbReaction('❤️'), 'love');
}); });
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { test('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
assert.strictEqual(await toDbReaction('❤'), 'love'); assert.strictEqual(await toDbReaction('❤'), 'love');
}); });
it('プリン以外の既存のリアクションは文字列化する laugh', async () => { test('プリン以外の既存のリアクションは文字列化する laugh', async () => {
assert.strictEqual(await toDbReaction('😆'), 'laugh'); assert.strictEqual(await toDbReaction('😆'), 'laugh');
}); });
it('プリン以外の既存のリアクションは文字列化する hmm', async () => { test('プリン以外の既存のリアクションは文字列化する hmm', async () => {
assert.strictEqual(await toDbReaction('🤔'), 'hmm'); assert.strictEqual(await toDbReaction('🤔'), 'hmm');
}); });
it('プリン以外の既存のリアクションは文字列化する surprise', async () => { test('プリン以外の既存のリアクションは文字列化する surprise', async () => {
assert.strictEqual(await toDbReaction('😮'), 'surprise'); assert.strictEqual(await toDbReaction('😮'), 'surprise');
}); });
it('プリン以外の既存のリアクションは文字列化する congrats', async () => { test('プリン以外の既存のリアクションは文字列化する congrats', async () => {
assert.strictEqual(await toDbReaction('🎉'), 'congrats'); assert.strictEqual(await toDbReaction('🎉'), 'congrats');
}); });
it('プリン以外の既存のリアクションは文字列化する angry', async () => { test('プリン以外の既存のリアクションは文字列化する angry', async () => {
assert.strictEqual(await toDbReaction('💢'), 'angry'); assert.strictEqual(await toDbReaction('💢'), 'angry');
}); });
it('プリン以外の既存のリアクションは文字列化する confused', async () => { test('プリン以外の既存のリアクションは文字列化する confused', async () => {
assert.strictEqual(await toDbReaction('😥'), 'confused'); assert.strictEqual(await toDbReaction('😥'), 'confused');
}); });
it('プリン以外の既存のリアクションは文字列化する rip', async () => { test('プリン以外の既存のリアクションは文字列化する rip', async () => {
assert.strictEqual(await toDbReaction('😇'), 'rip'); assert.strictEqual(await toDbReaction('😇'), 'rip');
}); });
it('それ以外はUnicodeのまま', async () => { test('それ以外はUnicodeのまま', async () => {
assert.strictEqual(await toDbReaction('🍅'), '🍅'); assert.strictEqual(await toDbReaction('🍅'), '🍅');
}); });
it('異体字セレクタ除去', async () => { test('異体字セレクタ除去', async () => {
assert.strictEqual(await toDbReaction('㊗️'), '㊗'); assert.strictEqual(await toDbReaction('㊗️'), '㊗');
}); });
it('異体字セレクタ除去 必要なし', async () => { test('異体字セレクタ除去 必要なし', async () => {
assert.strictEqual(await toDbReaction('㊗'), '㊗'); assert.strictEqual(await toDbReaction('㊗'), '㊗');
}); });
it('fallback - undefined', async () => { test('fallback - undefined', async () => {
assert.strictEqual(await toDbReaction(undefined), 'like'); assert.strictEqual(await toDbReaction(undefined), 'like');
}); });
it('fallback - null', async () => { test('fallback - null', async () => {
assert.strictEqual(await toDbReaction(null), 'like'); assert.strictEqual(await toDbReaction(null), 'like');
}); });
it('fallback - empty', async () => { test('fallback - empty', async () => {
assert.strictEqual(await toDbReaction(''), 'like'); assert.strictEqual(await toDbReaction(''), 'like');
}); });
it('fallback - unknown', async () => { test('fallback - unknown', async () => {
assert.strictEqual(await toDbReaction('unknown'), 'like'); assert.strictEqual(await toDbReaction('unknown'), 'like');
}); });
}); });
@@ -54,7 +54,7 @@ describe('FileInfoService', () => {
await app.close(); await app.close();
}); });
it('Empty file', async () => { test('Empty file', async () => {
const path = `${resources}/emptyfile`; const path = `${resources}/emptyfile`;
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;
@@ -74,7 +74,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Generic JPEG', async () => { test('Generic JPEG', async () => {
const path = `${resources}/Lenna.jpg`; const path = `${resources}/Lenna.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;
@@ -94,7 +94,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Generic APNG', async () => { test('Generic APNG', async () => {
const path = `${resources}/anime.png`; const path = `${resources}/anime.png`;
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;
@@ -114,7 +114,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Generic AGIF', async () => { test('Generic AGIF', async () => {
const path = `${resources}/anime.gif`; const path = `${resources}/anime.gif`;
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;
@@ -134,7 +134,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('PNG with alpha', async () => { test('PNG with alpha', async () => {
const path = `${resources}/with-alpha.png`; const path = `${resources}/with-alpha.png`;
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;
@@ -154,7 +154,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Generic SVG', async () => { test('Generic SVG', async () => {
const path = `${resources}/image.svg`; const path = `${resources}/image.svg`;
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;
@@ -174,7 +174,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('SVG with XML definition', async () => { test('SVG with XML definition', async () => {
// https://github.com/misskey-dev/misskey/issues/4413 // https://github.com/misskey-dev/misskey/issues/4413
const path = `${resources}/with-xml-def.svg`; const path = `${resources}/with-xml-def.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@@ -195,7 +195,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Dimension limit', async () => { test('Dimension limit', async () => {
const path = `${resources}/25000x25000.png`; const path = `${resources}/25000x25000.png`;
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;
@@ -215,7 +215,7 @@ describe('FileInfoService', () => {
}); });
}); });
it('Rotate JPEG', async () => { test('Rotate JPEG', async () => {
const path = `${resources}/rotate.jpg`; const path = `${resources}/rotate.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;
+2 -2
View File
@@ -35,7 +35,7 @@ describe('MetaService', () => {
await app.close(); await app.close();
}); });
it('fetch (cache)', async () => { test('fetch (cache)', async () => {
const db = app.get<DataSource>(DI.db); const db = app.get<DataSource>(DI.db);
const spy = jest.spyOn(db, 'transaction'); const spy = jest.spyOn(db, 'transaction');
@@ -45,7 +45,7 @@ describe('MetaService', () => {
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
}); });
it('fetch (force)', async () => { test('fetch (force)', async () => {
const db = app.get<DataSource>(DI.db); const db = app.get<DataSource>(DI.db);
const spy = jest.spyOn(db, 'transaction'); const spy = jest.spyOn(db, 'transaction');
+4 -4
View File
@@ -57,7 +57,7 @@ describe('RelayService', () => {
await app.close(); await app.close();
}); });
it('addRelay', async () => { test('addRelay', async () => {
const result = await relayService.addRelay('https://example.com'); const result = await relayService.addRelay('https://example.com');
expect(result.inbox).toBe('https://example.com'); expect(result.inbox).toBe('https://example.com');
@@ -68,7 +68,7 @@ describe('RelayService', () => {
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
}); });
it('listRelay', async () => { test('listRelay', async () => {
const result = await relayService.listRelay(); const result = await relayService.listRelay();
expect(result.length).toBe(1); expect(result.length).toBe(1);
@@ -76,7 +76,7 @@ describe('RelayService', () => {
expect(result[0].status).toBe('requesting'); expect(result[0].status).toBe('requesting');
}); });
it('removeRelay: succ', async () => { test('removeRelay: succ', async () => {
await relayService.removeRelay('https://example.com'); await relayService.removeRelay('https://example.com');
expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver).toHaveBeenCalled();
@@ -89,7 +89,7 @@ describe('RelayService', () => {
expect(list.length).toBe(0); expect(list.length).toBe(0);
}); });
it('removeRelay: fail', async () => { test('removeRelay: fail', async () => {
await expect(relayService.removeRelay('https://x.example.com')) await expect(relayService.removeRelay('https://x.example.com'))
.rejects.toThrow('relay not found'); .rejects.toThrow('relay not found');
}); });
+232
View File
@@ -0,0 +1,232 @@
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import rndstr from 'rndstr';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { CoreModule } from '@/core/CoreModule.js';
import { MetaService } from '@/core/MetaService.js';
import { genAid } from '@/misc/id/aid.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
describe('RoleService', () => {
let app: TestingModule;
let roleService: RoleService;
let usersRepository: UsersRepository;
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
let metaService: jest.Mocked<MetaService>;
function createUser(data: Partial<User> = {}) {
const un = rndstr('a-z0-9', 16);
return usersRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
username: un,
usernameLower: un,
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
}
function createRole(data: Partial<Role> = {}) {
return rolesRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
updatedAt: new Date(),
lastUsedAt: new Date(),
description: '',
...data,
})
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
}
async function assign(roleId: Role['id'], userId: User['id']) {
await roleAssignmentsRepository.insert({
id: genAid(new Date()),
createdAt: new Date(),
roleId,
userId,
});
}
beforeEach(async () => {
app = await Test.createTestingModule({
imports: [
GlobalModule,
],
providers: [
RoleService,
UserCacheService,
],
})
.useMocker((token) => {
if (token === MetaService) {
return { fetch: jest.fn() };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
app.enableShutdownHooks();
roleService = app.get<RoleService>(RoleService);
usersRepository = app.get<UsersRepository>(DI.usersRepository);
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
});
afterEach(async () => {
await Promise.all([
app.get(DI.metasRepository).delete({}),
usersRepository.delete({}),
rolesRepository.delete({}),
roleAssignmentsRepository.delete({}),
]);
await app.close();
});
describe('getUserPolicies', () => {
test('instance default policies', async () => {
const user = await createUser();
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(false);
});
test('instance default policies 2', async () => {
const user = await createUser();
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: true,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(true);
});
test('with role', async () => {
const user = await createUser();
const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
useDefault: false,
priority: 0,
value: true,
},
},
});
await assign(role.id, user.id);
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.canManageCustomEmojis).toBe(true);
});
test('priority', async () => {
const user = await createUser();
const role1 = await createRole({
name: 'role1',
policies: {
driveCapacityMb: {
useDefault: false,
priority: 0,
value: 200,
},
},
});
const role2 = await createRole({
name: 'role2',
policies: {
driveCapacityMb: {
useDefault: false,
priority: 1,
value: 100,
},
},
});
await assign(role1.id, user.id);
await assign(role2.id, user.id);
metaService.fetch.mockResolvedValue({
policies: {
driveCapacityMb: 50,
},
} as any);
const result = await roleService.getUserPolicies(user.id);
expect(result.driveCapacityMb).toBe(100);
});
test('conditional role', async () => {
const user1 = await createUser({
createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
});
const user2 = await createUser({
createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
followersCount: 10,
});
const role = await createRole({
name: 'a',
policies: {
canManageCustomEmojis: {
useDefault: false,
priority: 0,
value: true,
},
},
target: 'conditional',
condFormula: {
type: 'and',
values: [{
type: 'followersMoreThanOrEq',
value: 10,
}, {
type: 'createdMoreThan',
sec: 60 * 60 * 24 * 7,
}],
},
});
metaService.fetch.mockResolvedValue({
policies: {
canManageCustomEmojis: false,
},
} as any);
const user1Policies = await roleService.getUserPolicies(user1.id);
const user2Policies = await roleService.getUserPolicies(user2.id);
expect(user1Policies.canManageCustomEmojis).toBe(false);
expect(user2Policies.canManageCustomEmojis).toBe(true);
});
});
});
+18 -18
View File
@@ -78,7 +78,7 @@ describe('Chart', () => {
if (db) await db.destroy(); if (db) await db.destroy();
}); });
it('Can updates', async () => { test('Can updates', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -102,7 +102,7 @@ describe('Chart', () => {
}); });
}); });
it('Can updates (dec)', async () => { test('Can updates (dec)', async () => {
await testChart.decrement(); await testChart.decrement();
await testChart.save(); await testChart.save();
@@ -126,7 +126,7 @@ describe('Chart', () => {
}); });
}); });
it('Empty chart', async () => { test('Empty chart', async () => {
const chartHours = await testChart.getChart('hour', 3, null); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null); const chartDays = await testChart.getChart('day', 3, null);
@@ -147,7 +147,7 @@ describe('Chart', () => {
}); });
}); });
it('Can updates at multiple times at same time', async () => { test('Can updates at multiple times at same time', async () => {
await testChart.increment(); await testChart.increment();
await testChart.increment(); await testChart.increment();
await testChart.increment(); await testChart.increment();
@@ -173,7 +173,7 @@ describe('Chart', () => {
}); });
}); });
it('複数回saveされてもデータの更新は一度だけ', async () => { test('複数回saveされてもデータの更新は一度だけ', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
await testChart.save(); await testChart.save();
@@ -199,7 +199,7 @@ describe('Chart', () => {
}); });
}); });
it('Can updates at different times', async () => { test('Can updates at different times', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -230,7 +230,7 @@ describe('Chart', () => {
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip // 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/* /*
it('Can updates at different times without save', async () => { test('Can updates at different times without save', async () => {
await testChart.increment(); await testChart.increment();
clock.tick('01:00:00'); clock.tick('01:00:00');
@@ -259,7 +259,7 @@ describe('Chart', () => {
}); });
*/ */
it('Can padding', async () => { test('Can padding', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -289,7 +289,7 @@ describe('Chart', () => {
}); });
// 要求された範囲にログがひとつもない場合でもパディングできる // 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async () => { test('Can padding from past range', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -317,7 +317,7 @@ describe('Chart', () => {
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190 // Issue #3190
it('Can padding from past range 2', async () => { test('Can padding from past range 2', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -346,7 +346,7 @@ describe('Chart', () => {
}); });
}); });
it('Can specify offset', async () => { test('Can specify offset', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@@ -375,7 +375,7 @@ describe('Chart', () => {
}); });
}); });
it('Can specify offset (floor time)', async () => { test('Can specify offset (floor time)', async () => {
clock.tick('00:30:00'); clock.tick('00:30:00');
await testChart.increment(); await testChart.increment();
@@ -407,7 +407,7 @@ describe('Chart', () => {
}); });
describe('Grouped', () => { describe('Grouped', () => {
it('Can updates', async () => { test('Can updates', async () => {
await testGroupedChart.increment('alice'); await testGroupedChart.increment('alice');
await testGroupedChart.save(); await testGroupedChart.save();
@@ -451,7 +451,7 @@ describe('Chart', () => {
}); });
describe('Unique increment', () => { describe('Unique increment', () => {
it('Can updates', async () => { test('Can updates', async () => {
await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob'); await testUniqueChart.uniqueIncrement('bob');
@@ -470,7 +470,7 @@ describe('Chart', () => {
}); });
describe('Intersection', () => { describe('Intersection', () => {
it('条件が満たされていない場合はカウントされない', async () => { test('条件が満たされていない場合はカウントされない', async () => {
await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob'); await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
@@ -492,7 +492,7 @@ describe('Chart', () => {
}); });
}); });
it('条件が満たされている場合にカウントされる', async () => { test('条件が満たされている場合にカウントされる', async () => {
await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob'); await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
@@ -518,7 +518,7 @@ describe('Chart', () => {
}); });
describe('Resync', () => { describe('Resync', () => {
it('Can resync', async () => { test('Can resync', async () => {
testChart.total = 1; testChart.total = 1;
await testChart.resync(); await testChart.resync();
@@ -543,7 +543,7 @@ describe('Chart', () => {
}); });
}); });
it('Can resync (2)', async () => { test('Can resync (2)', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
+25 -24
View File
@@ -11,19 +11,19 @@
"@rollup/plugin-alias": "4.0.3", "@rollup/plugin-alias": "4.0.3",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.2", "@syuilo/aiscript": "0.12.4",
"@tabler/icons-webfont": "^2.0.0", "@tabler/icons-webfont": "2.1.2",
"@vitejs/plugin-vue": "4.0.0", "@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.45", "@vue/compiler-sfc": "3.2.47",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"broadcast-channel": "4.20.2", "broadcast-channel": "4.20.2",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"canvas-confetti": "^1.6.0", "canvas-confetti": "1.6.0",
"chart.js": "4.2.0", "chart.js": "4.2.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^1.3.0", "chartjs-chart-matrix": "1.3.0",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.0", "chartjs-plugin-zoom": "2.0.0",
"compare-versions": "5.0.1", "compare-versions": "5.0.1",
@@ -31,39 +31,40 @@
"date-fns": "2.29.3", "date-fns": "2.29.3",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eventemitter3": "5.0.0", "eventemitter3": "5.0.0",
"gsap": "^3.11.4", "gsap": "3.11.4",
"idb-keyval": "6.2.0", "idb-keyval": "6.2.0",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"misskey-js": "0.0.14", "misskey-js": "0.0.15",
"photoswipe": "5.3.4", "photoswipe": "5.3.5",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.3.0", "punycode": "2.3.0",
"querystring": "0.2.1", "querystring": "0.2.1",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rollup": "3.10.1", "rollup": "3.12.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "^2.8.1", "sanitize-html": "2.9.0",
"sass": "1.57.1", "sass": "1.58.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.148.0", "three": "0.149.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.5.2", "tinycolor2": "1.5.2",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "4.9.4", "typescript": "4.9.5",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.8.0", "vanilla-tilt": "1.8.0",
"vite": "4.0.4", "vue-plyr": "7.0.0",
"vue": "3.2.45", "vite": "4.1.1",
"vue": "3.2.47",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next" "vuedraggable": "next"
}, },
@@ -73,25 +74,25 @@
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
"@types/node": "^18.11.18", "@types/node": "18.11.18",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0", "@types/sanitize-html": "2.8.0",
"@types/seedrandom": "3.0.4", "@types/seedrandom": "3.0.4",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.0", "@types/uuid": "9.0.0",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.49.0", "@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.49.0", "@typescript-eslint/parser": "5.50.0",
"@vue/runtime-core": "3.2.45", "@vue/runtime-core": "3.2.47",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.5.1",
"eslint": "8.32.0", "eslint": "8.33.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.9.0", "eslint-plugin-vue": "9.9.0",
"start-server-and-test": "1.15.3", "start-server-and-test": "1.15.3",
"vue-eslint-parser": "^9.1.0", "vue-eslint-parser": "9.1.0",
"vue-tsc": "^1.0.24" "vue-tsc": "1.0.24"
} }
} }

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