Compare commits

...

85 Commits

Author SHA1 Message Date
syuilo baf65bfa69 Merge branch 'develop' 2023-02-05 20:55:51 +09:00
syuilo 6501f80fc7 13.4.0 2023-02-05 20:55:42 +09:00
syuilo b037f6566b Add Thai to language selection
Resolve #9809
2023-02-05 20:53:40 +09:00
syuilo 0ec8ebeba3 fix(client): tweak notification style
Fix #8633
2023-02-05 20:47:27 +09:00
syuilo af1c9251fc chore(client): add type check 2023-02-05 20:38:33 +09:00
Masaya Suzuki 4ad399c593 fix: テスト実行時のDB立ち上げコマンド修正 (#9804) 2023-02-05 20:34:22 +09:00
syuilo 55a9646f23 New Crowdin updates (#9798)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Lao)
2023-02-05 20:32:19 +09:00
syuilo 46017f5725 Update CHANGELOG.md 2023-02-05 20:32:12 +09:00
Caipira c20ce12f86 enhance(client): add webhook delete button (#9806) 2023-02-05 20:31:38 +09:00
syuilo 1e28db2396 Update CHANGELOG.md 2023-02-05 20:30:46 +09:00
syuilo 5f3640c7fd fix(client): validate input response in aiscript 2023-02-05 20:29:10 +09:00
syuilo d65e5f6794 単なるラッキーの獲得確立を調整 2023-02-05 14:38:21 +09:00
syuilo e67d7bc0ea tweak animation 2023-02-05 14:35:00 +09:00
syuilo 1139632f95 fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正 2023-02-05 14:30:07 +09:00
syuilo b51a8c3f82 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-05 14:25:39 +09:00
syuilo 0d7256678e fix(server): validate filename and emoji name to improve security 2023-02-05 14:25:37 +09:00
Masaya Suzuki eea33d07fd fix: aptのキャッシュを削除しないようにする (#9803) 2023-02-05 14:15:59 +09:00
Masaya Suzuki f599337320 DockleのCI追加 (#9568)
* Dockerイメージ検査のCI追加

* Add cp

* step分離

* step分離

* rm depends_on

* Dockle実行時に必要なイメージタグ付与処理をCI内で行う

* 末尾に移動

* Add comment

* .git削除処理をビルドステージに移動

* docker-compose.yml作成処理追加

* aptのキャッシュ削除処理追加

* ヘルスチェック用スクリプト追加

* yqインストール処理修正

* Add ca-certificates

* yqインストール処理をビルドステージに移動

* インデントを揃える

* インデントをタブに変更
2023-02-05 14:04:02 +09:00
Takuya Yoshida 7df019db0e BuildX設定漏れ修正 (#9741)
* BuildX設定漏れ

* Update .github/workflows/docker-develop.yml

Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>

---------

Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
2023-02-05 14:03:26 +09:00
futchitwo 04f92bd688 feat: timeline page for non-login users (#9795) 2023-02-05 14:02:54 +09:00
MeiMei 505ecf6c1f Deny UNIX domain socket (#9802)
* Deny UNIX domain socket

* got v12ならこれが使える?
2023-02-05 13:51:59 +09:00
Masaya Suzuki c9ec08704e fix: インラインコードを折り返して表示する (#9801) 2023-02-05 13:33:21 +09:00
syuilo 6a3039f7b7 feat: ロールにアイコンを設定してユーザー名の横に表示できるように
Resolve #9761
2023-02-05 10:37:03 +09:00
tamaina 868c8fffb3 update CHANGELOG.md 2023-02-04 15:05:13 +00:00
tamaina faed3b438e fix(server): clean up file in FileServer 2023-02-04 13:46:19 +00:00
syuilo 6c982629ea Merge branch 'develop' 2023-02-04 19:19:57 +09:00
syuilo 110bbbc7dc 13.3.4 2023-02-04 19:19:48 +09:00
syuilo 4ad0345f20 fix(server): cannot follow user 2023-02-04 19:19:30 +09:00
syuilo 9d84214462 Merge branch 'develop' 2023-02-04 18:22:08 +09:00
syuilo 3f199c7113 13.3.3 2023-02-04 18:22:00 +09:00
syuilo e9417fb741 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-04 18:21:23 +09:00
syuilo ee74df6823 fix(server): improve security 2023-02-04 18:21:07 +09:00
syuilo 26630bae81 New translations ja-JP.yml (Chinese Simplified) (#9792) 2023-02-04 18:19:49 +09:00
syuilo 9bde9edcf6 Merge branch 'develop' 2023-02-04 14:23:38 +09:00
syuilo a12f07c42b 13.3.2 2023-02-04 14:23:29 +09:00
syuilo e7334c4fb0 Update CHANGELOG.md 2023-02-04 14:21:08 +09:00
syuilo 38f9d1e764 fix(client): validate urls to improve security 2023-02-04 14:20:07 +09:00
tamaina 2dfed75402 perf(server): improvement of external mediaProxy (#9787)
* perf(server): improvement of external mediaProxy

* add a comment

* ✌️

* /filesでsharpの処理を行わずリダイレクトする

* fix

* thumbnail => static

* Fix #9788

* add avatar mode

* add url

* fix

* static.webp

* remove encodeURIComponent from media proxy path

* remove existance check
2023-02-04 13:38:51 +09:00
syuilo 0c12e80106 perf(server): cache blocking 2023-02-04 12:40:40 +09:00
syuilo b7522f69e7 fix typo 2023-02-04 10:02:03 +09:00
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
170 changed files with 3351 additions and 4269 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
+1 -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
@@ -135,6 +130,7 @@ proxyBypassHosts:
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 #proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy # Media Proxy
# Reference Implementation: https://github.com/misskey-dev/media-proxy
#mediaProxy: https://example.com/proxy #mediaProxy: https://example.com/proxy
# Proxy remote files (default: false) # Proxy remote files (default: false)
+6
View File
@@ -16,9 +16,15 @@ files/
misskey-assets/ misskey-assets/
fluent-emojis/ fluent-emojis/
.pnp.* .pnp.*
# .yarn関連
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml
+3
View File
@@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003
+2
View File
@@ -14,6 +14,8 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
+30
View File
@@ -0,0 +1,30 @@
---
name: Dockle
on:
push:
branches:
- master
- develop
pull_request:
jobs:
dockle:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v3.2.0
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose.yml.example ./docker-compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
- run: |
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
echo "> ${cmd}"
eval "${cmd}"
+50
View File
@@ -8,6 +8,56 @@
You should also include the user name that made the change. You should also include the user name that made the change.
--> -->
## 13.4.0 (2023/02/05)
### Improvements
- ロールにアイコンを設定してユーザー名の横に表示できるように
- feat: timeline page for non-login users
- 実績の単なるラッキーの獲得確立を調整
- Add Thai language support
### Bugfixes
- fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正
- fix(server): clean up file in FileServer
- fix(server): Deny UNIX domain socket
- fix(server): validate filename and emoji name to improve security
- fix(client): validate input response in aiscript
- fix(client): add webhook delete button
- fix(client): tweak notification style
- fix(client): インラインコードを折り返して表示する
## 13.3.3 (2023/02/04)
### Bugfixes
- Server: improve security
## 13.3.2 (2023/02/04)
### Improvements
- 外部メディアプロキシへの対応を強化しました
外部メディアプロキシのFastify実装を作りました
https://github.com/misskey-dev/media-proxy
- Server: improve performance
### Bugfixes
- Client: validate urls to improve security
## 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) ## 13.2.6 (2023/02/01)
### Changes ### Changes
+9 -9
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.
@@ -121,18 +121,18 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. Prepare DB/Redis for testing.
``` ```
docker-compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/docker-compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
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下に移してください
+9 -2
View File
@@ -8,7 +8,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \ && apt-get update \
&& apt-get install -yqq --no-install-recommends \ && apt-get install -yqq --no-install-recommends \
build-essential build-essential wget ca-certificates \
&& wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq \
&& chmod +x /usr/bin/yq
RUN corepack enable RUN corepack enable
@@ -29,6 +31,7 @@ ARG NODE_ENV=production
RUN git submodule update --init RUN git submodule update --init
RUN pnpm build RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner FROM node:${NODE_VERSION}-slim AS runner
@@ -44,11 +47,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
ffmpeg tini \ ffmpeg tini \
&& 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 \
&& find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
&& find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \;
USER misskey USER misskey
WORKDIR /misskey WORKDIR /misskey
COPY --from=builder /usr/bin/yq /usr/bin/yq
COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=builder /misskey/built ./built COPY --chown=misskey:misskey --from=builder /misskey/built ./built
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
@@ -58,5 +64,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue
COPY --chown=misskey:misskey . ./ COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production ENV NODE_ENV=production
HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "migrateandstart"] CMD ["pnpm", "run", "migrateandstart"]
+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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
PORT=$(yq '.port' /misskey/.config/default.yml)
curl -s -S -o /dev/null "http://localhost:${PORT}"
+5 -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"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Rollenvorlage" baseRole: "Rollenvorlage"
useBaseValue: "Wert der Rollenvorlage verwenden" useBaseValue: "Wert der Rollenvorlage verwenden"
chooseRoleToAssign: "Zuzuweisende Rolle auswählen" chooseRoleToAssign: "Zuzuweisende Rolle auswählen"
iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten." descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
priority: "Priorität" priority: "Priorität"
+4 -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"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Role template" baseRole: "Role template"
useBaseValue: "Use role template value" useBaseValue: "Use role template value"
chooseRoleToAssign: "Select the role to assign" chooseRoleToAssign: "Select the role to assign"
iconUrl: "Icon URL"
asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role" canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users." descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority" priority: "Priority"
+324 -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,326 @@ 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"
iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
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 +1637,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 +1745,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 +1827,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"
+1
View File
@@ -34,6 +34,7 @@ const languages = [
'pt-PT', 'pt-PT',
'ru-RU', 'ru-RU',
'sk-SK', 'sk-SK',
'th-TH',
'ug-CN', 'ug-CN',
'uk-UA', 'uk-UA',
'vi-VN', 'vi-VN',
+5 -2
View File
@@ -1148,7 +1148,7 @@ _achievements:
description: "ここをクリックした" description: "ここをクリックした"
_justPlainLucky: _justPlainLucky:
title: "単なるラッキー" title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得" description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo: _setNameToSyuilo:
title: "神様コンプレックス" title: "神様コンプレックス"
description: "名前を syuilo に設定した" description: "名前を syuilo に設定した"
@@ -1184,7 +1184,7 @@ _role:
description: "ロールの説明" description: "ロールの説明"
permission: "ロールの権限" permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。" descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル" manual: "マニュアル"
conditional: "コンディショナル" conditional: "コンディショナル"
@@ -1197,6 +1197,9 @@ _role:
baseRole: "ベースロール" baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用" useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択" chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度" priority: "優先度"
+2
View File
@@ -0,0 +1,2 @@
---
_lang_: "ພາສາລາວ"
+3
View File
@@ -1195,6 +1195,9 @@ _role:
baseRole: "บทบาทพื้นฐาน" baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ" priority: "ลำดับความสำคัญ"
+11 -1
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: "Скрипт"
@@ -1084,22 +1084,32 @@ _achievements:
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)" description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
_viewInstanceChart: _viewInstanceChart:
title: "Аналітик" title: "Аналітик"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "Вивести \"hello world\" у Скретчпаді"
_clickedClickHere: _clickedClickHere:
title: "Натисніть тут" title: "Натисніть тут"
description: "Натиснуто тут" description: "Натиснуто тут"
_justPlainLucky:
title: "Просто вдача"
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
_setNameToSyuilo: _setNameToSyuilo:
title: "Комплекс бога" title: "Комплекс бога"
description: "Встановлено ім'я \"syuilo\"" 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: _loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року" description: "Увійшли в перший день року"
_brainDiver: _brainDiver:
title: "Brain Diver" title: "Brain Diver"
+29 -8
View File
@@ -1023,17 +1023,23 @@ _achievements:
title: "定期联系Ⅲ" title: "定期联系Ⅲ"
description: "总登录天数400天" description: "总登录天数400天"
_login500: _login500:
title: "老熟人Ⅰ"
description: "总登录天数500天" description: "总登录天数500天"
flavor: "诸君,我喜欢贴文" flavor: "诸君,我喜欢贴文"
_login600: _login600:
title: "老熟人Ⅱ"
description: "总登录天数600天" description: "总登录天数600天"
_login700: _login700:
title: "老熟人Ⅲ"
description: "总登录天数700天" description: "总登录天数700天"
_login800: _login800:
title: "帖子大师Ⅰ"
description: "总登录天数800天" description: "总登录天数800天"
_login900: _login900:
title: "帖子大师Ⅱ"
description: "总登录天数900天" description: "总登录天数900天"
_login1000: _login1000:
title: "帖子大师Ⅲ"
description: "总登录天数1000天" description: "总登录天数1000天"
flavor: "感谢您使用Misskey" flavor: "感谢您使用Misskey"
_noteClipped1: _noteClipped1:
@@ -1072,19 +1078,22 @@ _achievements:
description: "第一次被关注" description: "第一次被关注"
_followers10: _followers10:
title: "关注我吧!" title: "关注我吧!"
description: "关注者超过10" description: "拥有超过10名关注者"
_followers50: _followers50:
title: "三五成群" title: "三五成群"
description: "关注者超过50" description: "拥有超过50名关注者"
_followers100: _followers100:
title: "胜友如云" title: "胜友如云"
description: "关注者超过100" description: "拥有超过100名关注者"
_followers300: _followers300:
title: "排列成行" title: "排列成行"
description: "关注者超过300" description: "拥有超过300名关注者"
_followers500: _followers500:
title: "风向标" title: "信号塔"
description: "关注者超过500" description: "拥有超过500名关注者"
_followers1000:
title: "大影响家"
description: "拥有超过1000名关注者"
_collectAchievements30: _collectAchievements30:
title: "成就收藏家" title: "成就收藏家"
description: "获得超过30个成就" description: "获得超过30个成就"
@@ -1096,6 +1105,7 @@ _achievements:
description: "发布\"I ❤ #Misskey\"帖子" description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队" flavor: "感谢您使用 Misskey by 开发团队"
_foundTreasure: _foundTreasure:
title: "寻宝"
description: "发现了隐藏的宝藏" description: "发现了隐藏的宝藏"
_client30min: _client30min:
title: "休息一下!" title: "休息一下!"
@@ -1104,7 +1114,7 @@ _achievements:
title: "无话可说" title: "无话可说"
description: "发帖后一分钟内就将其删除" description: "发帖后一分钟内就将其删除"
_postedAtLateNight: _postedAtLateNight:
title: "夜行者" title: "夜猫子"
description: "深夜发布帖子" description: "深夜发布帖子"
flavor: "差不多该去睡了喔。" flavor: "差不多该去睡了喔。"
_postedAt0min0sec: _postedAt0min0sec:
@@ -1114,13 +1124,21 @@ _achievements:
_selfQuote: _selfQuote:
title: "自我提及" title: "自我提及"
description: "引用了自己的帖子" description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "在AiScript控制台中输出 hello world"
_open3windows: _open3windows:
title: "多窗口" title: "多窗口"
description: "打开了三个或更多的窗口" description: "打开了三个或更多的窗口"
_driveFolderCircularReference: _driveFolderCircularReference:
title: "循环引用" title: "循环引用"
description: "试图对网盘中的文件夹进行循环嵌套"
_reactWithoutRead: _reactWithoutRead:
title: "有好好读过吗?" title: "有好好读过吗?"
description: "在含有100字以上的帖子被发出三秒内做出回应" description: "在含有100字以上的帖子被发出三秒内做出回应"
@@ -1129,7 +1147,7 @@ _achievements:
description: "点了这里" description: "点了这里"
_justPlainLucky: _justPlainLucky:
title: "超高校级的幸运" title: "超高校级的幸运"
description: "每10秒有0.01的概率获得" description: "每10秒有0.01的概率自动获得"
_setNameToSyuilo: _setNameToSyuilo:
title: "像神一样呐" title: "像神一样呐"
description: "将名称设定为syuilo" description: "将名称设定为syuilo"
@@ -1177,6 +1195,9 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色" chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级" priority: "优先级"
+3
View File
@@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色" chooseRoleToAssign: "選擇要指派的角色"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯監察員的成員" canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級" priority: "優先級"
+9 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.2.6", "version": "13.4.0",
"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.4.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`);
}
}
@@ -0,0 +1,13 @@
export class roleIconBadge1675557528704 {
name = 'roleIconBadge1675557528704'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`);
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`);
}
}
+34 -35
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.7.0", "@fastify/static": "6.8.0",
"@fastify/view": "7.4.1", "@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.4",
"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,21 +125,21 @@
"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.29", "@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",
@@ -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.4.1", "jest": "29.4.1",
"jest-mock": "^29.4.1" "jest-mock": "29.4.1"
} }
} }
+10 -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;
@@ -92,6 +87,8 @@ export type Mixin = {
userAgent: string; userAgent: string;
clientEntry: string; clientEntry: string;
clientManifestExists: boolean; clientManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;
@@ -113,7 +110,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' } };
@@ -140,6 +137,13 @@ export function loadConfig() {
mixin.clientEntry = clientManifest['src/init.ts']; mixin.clientEntry = clientManifest['src/init.ts'];
mixin.clientManifestExists = clientManifestExists; mixin.clientManifestExists = clientManifestExists;
const externalMediaProxy = config.mediaProxy ?
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
: null;
const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`;
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy;
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
if (!config.redis.prefix) config.redis.prefix = mixin.host; if (!config.redis.prefix) config.redis.prefix = mixin.host;
return Object.assign(config, mixin); return Object.assign(config, mixin);
+4 -14
View File
@@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { Cache } from '@/misc/cache.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
export class AntennaService implements OnApplicationShutdown { export class AntennaService implements OnApplicationShutdown {
private antennasFetched: boolean; private antennasFetched: boolean;
private antennas: Antenna[]; private antennas: Antenna[];
private blockingCache: Cache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisSubscriber)
@@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private antennaEntityService: AntennaEntityService, private antennaEntityService: AntennaEntityService,
) { ) {
this.antennasFetched = false; this.antennasFetched = false;
this.antennas = []; this.antennas = [];
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
this.redisSubscriber.on('message', this.onRedisMessage); this.redisSubscriber.on('message', this.onRedisMessage);
} }
@@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
read: read, read: read,
}); });
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) { if (!read) {
const mutings = await this.mutingsRepository.find({ const mutings = await this.mutingsRepository.find({
@@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
setTimeout(async () => { setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) { if (unread) {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name }, antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note), note: await this.noteEntityService.pack(note),
@@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false; if (note.visibility === 'followers') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
if (blockings.some(blocking => blocking === antenna.userId)) return false;
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
-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,
@@ -26,7 +26,7 @@ export class CreateNotificationService {
private notificationEntityService: NotificationEntityService, private notificationEntityService: NotificationEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
) { ) {
} }
@@ -60,7 +60,7 @@ export class CreateNotificationService {
const packed = await this.notificationEntityService.pack(notification, {}); const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event // Publish notification event
this.globalEventServie.publishMainStream(notifieeId, 'notification', packed); this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => { setTimeout(async () => {
@@ -77,7 +77,7 @@ export class CreateNotificationService {
} }
//#endregion //#endregion
this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed); this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
@@ -120,7 +120,7 @@ export class CustomEmojiService {
const url = isLocal const url = isLocal
? emojiUrl ? emojiUrl
: this.config.proxyRemoteFiles : this.config.proxyRemoteFiles
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}` ? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl; : emojiUrl;
return url; return url;
@@ -14,7 +14,7 @@ export class DeleteAccountService {
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
} }
@@ -38,6 +38,6 @@ export class DeleteAccountService {
}); });
// Terminate streaming // Terminate streaming
this.globalEventServie.publishUserEvent(user.id, 'terminate', {}); this.globalEventService.publishUserEvent(user.id, 'terminate', {});
} }
} }
@@ -60,6 +60,7 @@ export class DownloadService {
retry: { retry: {
limit: 0, limit: 0,
}, },
enableUnixSockets: false,
}).on('response', (res: Got.Response) => { }).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) { if (this.isPrivateIp(res.ip)) {
@@ -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
+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);
} }
} }
@@ -175,7 +175,7 @@ export class NoteCreateService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private queueService: QueueService, private queueService: QueueService,
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
@@ -535,7 +535,7 @@ export class NoteCreateService {
// Pack the note // Pack the note
const noteObj = await this.noteEntityService.pack(note); const noteObj = await this.noteEntityService.pack(note);
this.globalEventServie.publishNotesStream(noteObj); this.globalEventService.publishNotesStream(noteObj);
this.webhookService.getActiveWebhooks().then(webhooks => { this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
@@ -561,7 +561,7 @@ export class NoteCreateService {
if (!threadMuted) { if (!threadMuted) {
nm.push(data.reply.userId, 'reply'); nm.push(data.reply.userId, 'reply');
this.globalEventServie.publishMainStream(data.reply.userId, 'reply', noteObj); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -584,7 +584,7 @@ export class NoteCreateService {
// Publish event // Publish event
if ((user.id !== data.renote.userId) && data.renote.userHost === null) { if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
this.globalEventServie.publishMainStream(data.renote.userId, 'renote', noteObj); this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -684,7 +684,7 @@ export class NoteCreateService {
detail: true, detail: true,
}); });
this.globalEventServie.publishMainStream(u.id, 'mention', detailPackedNote); this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -34,7 +34,7 @@ export class NoteDeleteService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private relayService: RelayService, private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@@ -63,7 +63,7 @@ export class NoteDeleteService {
} }
if (!quiet) { if (!quiet) {
this.globalEventServie.publishNoteStream(note.id, 'deleted', { this.globalEventService.publishNoteStream(note.id, 'deleted', {
deletedAt: deletedAt, deletedAt: deletedAt,
}); });
+11 -17
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()
@@ -40,7 +40,7 @@ export class NoteReadService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private notificationService: NotificationService, private notificationService: NotificationService,
private antennaService: AntennaService, private antennaService: AntennaService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
@@ -87,13 +87,13 @@ export class NoteReadService {
if (exist == null) return; if (exist == null) return;
if (params.isMentioned) { if (params.isMentioned) {
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
} }
if (params.isSpecified) { if (params.isSpecified) {
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
} }
if (note.channelId) { if (note.channelId) {
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
} }
}, 2000); }, 2000);
} }
@@ -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);
} }
} }
@@ -161,7 +155,7 @@ export class NoteReadService {
}).then(mentionsCount => { }).then(mentionsCount => {
if (mentionsCount === 0) { if (mentionsCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
}); });
@@ -171,7 +165,7 @@ export class NoteReadService {
}).then(specifiedCount => { }).then(specifiedCount => {
if (specifiedCount === 0) { if (specifiedCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
} }
}); });
@@ -181,7 +175,7 @@ export class NoteReadService {
}).then(channelNoteCount => { }).then(channelNoteCount => {
if (channelNoteCount === 0) { if (channelNoteCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllChannels'); this.globalEventService.publishMainStream(userId, 'readAllChannels');
} }
}); });
@@ -206,14 +200,14 @@ export class NoteReadService {
}); });
if (count === 0) { if (count === 0) {
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
} }
} }
this.userEntityService.getHasUnreadAntenna(userId).then(unread => { this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) { if (!unread) {
this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined); this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
} }
}); });
+7 -13
View File
@@ -1,17 +1,17 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import type { CacheableUser } from '@/models/entities/User.js'; import type { CacheableUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
@Injectable() @Injectable()
export class PollService { export class PollService {
@@ -28,14 +28,11 @@ export class PollService {
@Inject(DI.pollVotesRepository) @Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository, private pollVotesRepository: PollVotesRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private relayService: RelayService, private relayService: RelayService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private userBlockingService: UserBlockingService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
) { ) {
@@ -52,11 +49,8 @@ export class PollService {
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({ const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
blockerId: note.userId, if (blocked) {
blockeeId: user.id,
});
if (block) {
throw new Error('blocked'); throw new Error('blocked');
} }
} }
@@ -88,7 +82,7 @@ export class PollService {
const index = choice + 1; // In SQL, array index is 1 based const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventServie.publishNoteStream(note.id, 'pollVoted', { this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
choice: choice, choice: choice,
userId: user.id, userId: user.id,
}); });
+8 -9
View File
@@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from './UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@@ -73,8 +74,9 @@ export class ReactionService {
private metaService: MetaService, private metaService: MetaService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
@@ -86,11 +88,8 @@ export class ReactionService {
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) { public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) {
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({ const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
blockerId: note.userId, if (blocked) {
blockeeId: user.id,
});
if (block) {
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
} }
} }
@@ -157,7 +156,7 @@ export class ReactionService {
select: ['name', 'host', 'originalUrl', 'publicUrl'], select: ['name', 'host', 'originalUrl', 'publicUrl'],
}); });
this.globalEventServie.publishNoteStream(note.id, 'reacted', { this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction, reaction: decodedReaction.reaction,
emoji: emoji != null ? { emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
@@ -229,7 +228,7 @@ export class ReactionService {
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
this.globalEventServie.publishNoteStream(note.id, 'unreacted', { this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction, reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id, userId: user.id,
}); });
+13
View File
@@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
} }
/**
*
*/
@bindThis
public async getUserBadgeRoles(userId: User['id']) {
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
// コンディショナルロールも含めるのは負荷高そうだから一旦無し
return assignedBadgeRoles;
}
@bindThis @bindThis
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> { public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Redis from 'ioredis';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { CacheableUser, User } from '@/models/entities/User.js'; import type { CacheableUser, User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.js'; import type { Blocking } from '@/models/entities/Blocking.js';
@@ -7,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import logger from '@/logger.js';
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -15,12 +15,20 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable() @Injectable()
export class UserBlockingService { export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@@ -42,13 +50,44 @@ export class UserBlockingService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('user-block'); this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'blockingCreated': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
}
break;
}
case 'blockingDeleted': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
}
break;
}
default:
break;
}
}
} }
@bindThis @bindThis
@@ -72,6 +111,11 @@ export class UserBlockingService {
await this.blockingsRepository.insert(blocking); await this.blockingsRepository.insert(blocking);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox);
@@ -97,15 +141,15 @@ export class UserBlockingService {
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, { this.userEntityService.pack(followee, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
if (this.userEntityService.isLocalUser(follower)) { if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -152,8 +196,8 @@ export class UserBlockingService {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -210,10 +254,31 @@ export class UserBlockingService {
await this.blockingsRepository.delete(blocking.id); await this.blockingsRepository.delete(blocking.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
// deliver if remote bloking // deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
this.queueService.deliver(blocker, content, blockee.inbox); this.queueService.deliver(blocker, content, blockee.inbox);
} }
} }
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
where: {
blockerId,
},
select: ['blockeeId'],
}).then(records => records.map(record => record.blockeeId)));
return blockedUserIds.includes(blockeeId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
} }
@@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@@ -48,21 +49,18 @@ export class UserFollowingService {
@Inject(DI.followRequestsRepository) @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) { ) {
@@ -77,28 +75,22 @@ export class UserFollowingService {
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({ this.userBlockingService.checkBlocked(follower.id, followee.id),
blockerId: follower.id, this.userBlockingService.checkBlocked(followee.id, follower.id),
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
]); ]);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox); this.queueService.deliver(followee, content, follower.inbox);
return; return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.blockingsRepository.delete(blocking.id); await this.userBlockingService.unblock(follower, followee);
} else { } else {
// それ以外は単純に例外 // それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); if (blocking) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
} }
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
@@ -227,8 +219,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -242,7 +234,7 @@ export class UserFollowingService {
// Publish followed event // Publish followed event
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => { this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventServie.publishMainStream(followee.id, 'followed', packed); this.globalEventService.publishMainStream(followee.id, 'followed', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -288,8 +280,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@@ -357,18 +349,12 @@ export class UserFollowingService {
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({ this.userBlockingService.checkBlocked(follower.id, followee.id),
blockerId: follower.id, this.userBlockingService.checkBlocked(followee.id, follower.id),
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
]); ]);
if (blocking != null) throw new Error('blocking'); if (blocking) throw new Error('blocking');
if (blocked != null) throw new Error('blocked'); if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({ const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@@ -388,11 +374,11 @@ export class UserFollowingService {
// Publish receiveRequest event // Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成 // 通知を作成
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
@@ -440,7 +426,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@bindThis @bindThis
@@ -468,7 +454,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@bindThis @bindThis
@@ -583,8 +569,8 @@ export class UserFollowingService {
detail: true, detail: true,
}); });
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
+2 -2
View File
@@ -25,7 +25,7 @@ export class UserListService {
private idService: IdService, private idService: IdService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private roleService: RoleService, private roleService: RoleService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService, private proxyAccountService: ProxyAccountService,
) { ) {
} }
@@ -46,7 +46,7 @@ export class UserListService {
userListId: list.id, userListId: list.id,
} as UserListJoining); } as UserListJoining);
this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (this.userEntityService.isRemoteUser(target)) { if (this.userEntityService.isRemoteUser(target)) {
@@ -18,7 +18,7 @@ export class UserMutingService {
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventServie: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
} }
+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(),
@@ -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,
@@ -54,7 +54,7 @@ export class ChannelEntityService {
name: channel.name, name: channel.name,
description: channel.description, description: channel.description,
userId: channel.userId, userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null, bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
usersCount: channel.usersCount, usersCount: channel.usersCount,
notesCount: channel.notesCount, notesCount: channel.notesCount,
@@ -71,27 +71,41 @@ export class DriveFileEntityService {
} }
@bindThis @bindThis
public getPublicUrl(file: DriveFile, thumbnail = false): string | null { public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail
const proxiedUrl = (url: string) => appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
query({
url,
...(mode ? { [mode]: '1' } : {}),
})
);
// リモートかつメディアプロキシ // リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) { if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
return appendQuery(this.config.mediaProxy, query({ return proxiedUrl(file.uri);
url: file.uri,
thumbnail: thumbnail ? '1' : undefined,
}));
} }
// リモートかつ期限切れはローカルプロキシを試みる // リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey; const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
return `${this.config.url}/files/${key}`; const url = `${this.config.url}/files/${key}`;
if (mode === 'avatar') return proxiedUrl(url);
return url;
} }
} }
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type); const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); if (mode === 'static') {
return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null);
}
const url = file.webpublicUrl ?? file.url;
if (mode === 'avatar') return proxiedUrl(url);
return url;
} }
@bindThis @bindThis
@@ -166,8 +180,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, true), thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment, comment: file.comment,
folderId: file.folderId, folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
@@ -201,8 +215,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, true), thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment, comment: file.comment,
folderId: file.folderId, folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
@@ -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';
@@ -56,11 +56,13 @@ export class RoleEntityService {
name: role.name, name: role.name,
description: role.description, description: role.description,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
target: role.target, target: role.target,
condFormula: role.condFormula, condFormula: role.condFormula,
isPublic: role.isPublic, isPublic: role.isPublic,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
isModerator: role.isModerator, isModerator: role.isModerator,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies, policies: policies,
usersCount: assigns.length, usersCount: assigns.length,
@@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public async getAvatarUrl(user: User): Promise<string> { public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) { if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else if (user.avatarId) { } else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId }); const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user.id);
} }
@@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis @bindThis
public getAvatarUrlSync(user: User): string { public getAvatarUrlSync(user: User): string {
if (user.avatar) { if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id); return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
} else { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user.id);
} }
@@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined, } : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({
name: r.name,
iconUrl: r.iconUrl,
}))) : undefined,
...(opts.detail ? { ...(opts.detail ? {
url: profile!.url, url: profile!.url,
@@ -422,7 +427,7 @@ export class UserEntityService implements OnModuleInit {
createdAt: user.createdAt.toISOString(), createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null,
bannerBlurhash: user.banner?.blurhash ?? null, bannerBlurhash: user.banner?.blurhash ?? null,
isLocked: user.isLocked, isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
@@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit {
id: role.id, id: role.id,
name: role.name, name: role.name,
color: role.color, color: role.color,
iconUrl: role.iconUrl,
description: role.description, description: role.description,
isModerator: role.isModerator, isModerator: role.isModerator,
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
@@ -489,7 +495,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,
+1 -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
@@ -69,20 +67,6 @@ export default class Logger {
console.log(important ? chalk.bold(log) : log); console.log(important ? chalk.bold(log) : log);
if (level === 'error' && data) console.log(data); 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
+2
View File
@@ -1,5 +1,7 @@
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class Cache<T> { export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
@@ -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, '냥');
+19 -8
View File
@@ -132,11 +132,27 @@ 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 ArrayUnion<T> = T extends any ? Array<T> : 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 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 :
@@ -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,
@@ -102,6 +102,11 @@ export class Role {
}) })
public color: string | null; public color: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public iconUrl: string | null;
@Column('enum', { @Column('enum', {
enum: ['manual', 'conditional'], enum: ['manual', 'conditional'],
default: 'manual', default: 'manual',
@@ -118,6 +123,12 @@ export class Role {
}) })
public isPublic: boolean; public isPublic: boolean;
// trueの場合ユーザー名の横にバッジとして表示
@Column('boolean', {
default: false,
})
public asBadge: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })
@@ -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,
@@ -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(),
@@ -12,9 +12,9 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.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 ExportCustomEmojisProcessorService { export class ExportCustomEmojisProcessorService {
@@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService {
}); });
for (const emoji of customEmojis) { for (const emoji of customEmojis) {
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
this.logger.error(`invalid emoji name: ${emoji.name}`);
continue;
}
const ext = mime.extension(emoji.type ?? 'image/png'); const ext = mime.extension(emoji.type ?? 'image/png');
const fileName = emoji.name + (ext ? '.' + ext : ''); const fileName = emoji.name + (ext ? '.' + ext : '');
const emojiPath = path + '/' + fileName; const emojiPath = path + '/' + fileName;
@@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService {
for (const record of meta.emojis) { for (const record of meta.emojis) {
if (!record.downloaded) continue; if (!record.downloaded) continue;
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
this.logger.error(`invalid filename: ${record.fileName}`);
continue;
}
const emojiInfo = record.emoji; const emojiInfo = record.emoji;
const emojiPath = outputPath + '/' + record.fileName; const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({ await this.emojisRepository.delete({
@@ -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),
@@ -137,38 +137,42 @@ export class FileServerService {
try { try {
if (file.state === 'remote') { if (file.state === 'remote') {
const convertFile = async () => { let image: IImageStreamable | null = null;
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 (file.fileRole === 'thumbnail') {
if (['image/svg+xml'].includes(file.mime)) { if (isMimeImage(file.mime, 'sharp-convertible-image')) {
return this.imageProcessingService.convertToWebpStream( reply.header('Cache-Control', 'max-age=31536000, immutable');
file.path,
2048,
2048,
{ ...webpDefault, lossless: true }
)
}
}
return { const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
}
}
if (!image) {
image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
ext: file.ext, ext: file.ext,
type: file.mime, type: file.mime,
}; };
}; }
const image = await convertFile();
if ('pipe' in image.data && typeof image.data.pipe === 'function') { if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup // image.dataがstreamなら、stream終了後にcleanup
@@ -180,7 +184,6 @@ export class FileServerService {
} }
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); 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; return image.data;
} }
@@ -217,6 +220,23 @@ export class FileServerService {
return; return;
} }
if (this.config.externalMediaProxyEnabled) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
for (const [key, value] of Object.entries(request.query)) {
url.searchParams.append(key, value);
}
return await reply.redirect(
301,
url.toString(),
);
}
// Create temp file // Create temp file
const file = await this.getStreamAndTypeFromUrl(url); const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') { if (file === '404') {
@@ -236,7 +256,7 @@ export class FileServerService {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
let image: IImageStreamable | null = null; let image: IImageStreamable | null = null;
if ('emoji' in request.query && isConvertibleImage) { if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) { if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = { image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
@@ -246,7 +266,7 @@ export class FileServerService {
} else { } else {
const data = sharp(file.path, { animated: !('static' in request.query) }) const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({ .resize({
height: 128, height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.webp(webpDefault); .webp(webpDefault);
@@ -370,7 +390,7 @@ export class FileServerService {
@bindThis @bindThis
private async getFileFromKey(key: string): Promise< private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; } { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; 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; } | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404' | '404'
| '204' | '204'
@@ -392,6 +412,7 @@ export class FileServerService {
const result = await this.downloadAndDetectTypeFromUrl(file.uri); const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return { return {
...result, ...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file, file,
} }
@@ -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,
@@ -7,9 +7,6 @@ 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';
@@ -54,9 +51,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
ServerService, ServerService,
WellKnownServerService, WellKnownServerService,
GetterService, GetterService,
DiscordServerService,
GithubServerService,
TwitterServerService,
ChannelsService, ChannelsService,
ApiCallService, ApiCallService,
ApiLoggerService, ApiLoggerService,
+1 -1
View File
@@ -106,7 +106,7 @@ export class ServerService {
} }
} }
const url = new URL('/proxy/emoji.webp', this.config.url); const url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1'); url.searchParams.set('emoji', '1');
@@ -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,
@@ -19,11 +19,13 @@ export const paramDef = {
name: { type: 'string' }, name: { type: 'string' },
description: { type: 'string' }, description: { type: 'string' },
color: { type: 'string', nullable: true }, color: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string' }, target: { type: 'string' },
condFormula: { type: 'object' }, condFormula: { type: 'object' },
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
policies: { policies: {
type: 'object', type: 'object',
@@ -33,11 +35,13 @@ export const paramDef = {
'name', 'name',
'description', 'description',
'color', 'color',
'iconUrl',
'target', 'target',
'condFormula', 'condFormula',
'isPublic', 'isPublic',
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'asBadge',
'canEditMembersByModerator', 'canEditMembersByModerator',
'policies', 'policies',
], ],
@@ -64,11 +68,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
color: ps.color, color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target, target: ps.target,
condFormula: ps.condFormula, condFormula: ps.condFormula,
isPublic: ps.isPublic, isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator, isModerator: ps.isModerator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies, policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
@@ -27,11 +27,13 @@ export const paramDef = {
name: { type: 'string' }, name: { type: 'string' },
description: { type: 'string' }, description: { type: 'string' },
color: { type: 'string', nullable: true }, color: { type: 'string', nullable: true },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string' }, target: { type: 'string' },
condFormula: { type: 'object' }, condFormula: { type: 'object' },
isPublic: { type: 'boolean' }, isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
policies: { policies: {
type: 'object', type: 'object',
@@ -42,11 +44,13 @@ export const paramDef = {
'name', 'name',
'description', 'description',
'color', 'color',
'iconUrl',
'target', 'target',
'condFormula', 'condFormula',
'isPublic', 'isPublic',
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'asBadge',
'canEditMembersByModerator', 'canEditMembersByModerator',
'policies', 'policies',
], ],
@@ -73,11 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
description: ps.description, description: ps.description,
color: ps.color, color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target, target: ps.target,
condFormula: ps.condFormula, condFormula: ps.condFormula,
isPublic: ps.isPublic, isPublic: ps.isPublic,
isModerator: ps.isModerator, isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies, policies: ps.policies,
}); });
@@ -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,
@@ -193,6 +181,10 @@ export const meta = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
features: { features: {
type: 'object', type: 'object',
optional: true, nullable: false, optional: true, nullable: false,
@@ -225,18 +217,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,17 +305,14 @@ 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,
policies: { ...DEFAULT_POLICIES, ...instance.policies }, policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
...(ps.detail ? { ...(ps.detail ? {
pinnedPages: instance.pinnedPages, pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId, pinnedClipId: instance.pinnedClipId,
@@ -358,9 +335,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'],
}, },
], ],
@@ -5,8 +5,8 @@ import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { AchievementService } from '@/core/AchievementService.js'; import { AchievementService } from '@/core/AchievementService.js';
import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
tags: ['notes', 'favorites'], tags: ['notes', 'favorites'],
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id, userId: me.id,
}); });
if (note.userHost == null) { if (note.userHost == null && note.userId !== me.id) {
this.achievementService.create(note.userId, 'myNoteFavorited1'); this.achievementService.create(note.userId, 'myNoteFavorited1');
} }
}); });
@@ -1,6 +1,6 @@
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { IRemoteUser } from '@/models/entities/User.js'; import type { IRemoteUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@@ -77,9 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.pollsRepository) @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
@@ -93,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService, private createNotificationService: CreateNotificationService,
private userBlockingService: UserBlockingService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const createdAt = new Date(); const createdAt = new Date();
@@ -109,11 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking // Check blocking
if (note.userId !== me.id) { if (note.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({ const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
blockerId: note.userId, if (blocked) {
blockeeId: me.id,
});
if (block) {
throw new ApiError(meta.errors.youHaveBeenBlocked); throw new ApiError(meta.errors.youHaveBeenBlocked);
} }
} }
@@ -95,14 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try { try {
if (ps.tag) { if (ps.tag) {
if (!safeForSql(ps.tag)) throw 'Injection'; if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else { } else {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {
for (const tags of ps.query!) { for (const tags of ps.query!) {
qb.orWhere(new Brackets(qb => { qb.orWhere(new Brackets(qb => {
for (const tag of tags) { for (const tag of tags) {
if (!safeForSql(tag)) throw 'Injection'; if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
} }
})); }));
@@ -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));
}
}
+43 -37
View File
@@ -18,31 +18,28 @@ 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']>; blockingCreated: { blockerId: User['id']; blockeeId: User['id']; };
roleCreated: Serialized<Role>; blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; };
roleDeleted: Serialized<Role>; policiesUpdated: Role['policies'];
roleUpdated: Serialized<Role>; roleCreated: Role;
userRoleAssigned: Serialized<RoleAssignment>; roleDeleted: Role;
userRoleUnassigned: Serialized<RoleAssignment>; roleUpdated: Role;
webhookCreated: Serialized<Webhook>; userRoleAssigned: RoleAssignment;
webhookDeleted: Serialized<Webhook>; userRoleUnassigned: RoleAssignment;
webhookUpdated: Serialized<Webhook>; webhookCreated: Webhook;
antennaCreated: Serialized<Antenna>; webhookDeleted: Webhook;
antennaDeleted: Serialized<Antenna>; webhookUpdated: Webhook;
antennaUpdated: Serialized<Antenna>; antennaCreated: Antenna;
metaUpdated: Serialized<Meta>; antennaDeleted: Antenna;
antennaUpdated: Antenna;
metaUpdated: Meta;
} }
export interface BroadcastTypes { export interface BroadcastTypes {
@@ -210,63 +207,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'>>;
}; };
}; };
@@ -33,7 +33,7 @@ export class UrlPreviewService {
private wrap(url?: string): string | null { private wrap(url?: string): string | null {
return url != null return url != null
? url.match(/^https?:\/\//) ? url.match(/^https?:\/\//)
? `${this.config.url}/proxy/preview.webp?${query({ ? `${this.config.mediaProxy}/preview.webp?${query({
url, url,
preview: '1', preview: '1',
})}` })}`
@@ -73,6 +73,14 @@ export class UrlPreviewService {
}); });
this.logger.succ(`Got preview of ${url}: ${summary.title}`); this.logger.succ(`Got preview of ${url}: ${summary.title}`);
if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
summary.icon = this.wrap(summary.icon); summary.icon = this.wrap(summary.icon);
summary.thumbnail = this.wrap(summary.thumbnail); summary.thumbnail = this.wrap(summary.thumbnail);
+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,

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