Compare commits
	
		
			121 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ff97a003d1 | ||
|   | 53c92e3e23 | ||
|   | 13d13bc2f6 | ||
|   | 03744a25ed | ||
|   | eac3bf8bff | ||
|   | 2e1fbb5b16 | ||
|   | 98b3517d36 | ||
|   | dee662705e | ||
|   | 0da0cc80b9 | ||
|   | 650187deaf | ||
|   | 2e565cac2c | ||
|   | ac7537278c | ||
|   | f9a2e98831 | ||
|   | 54f789bd55 | ||
|   | 5ac9d13516 | ||
|   | 2be1a39d13 | ||
|   | f3c5edc852 | ||
|   | 30704e6de8 | ||
|   | 41932ac409 | ||
|   | 9843c596d8 | ||
|   | baf65bfa69 | ||
|   | 6501f80fc7 | ||
|   | b037f6566b | ||
|   | 0ec8ebeba3 | ||
|   | af1c9251fc | ||
|   | 4ad399c593 | ||
|   | 55a9646f23 | ||
|   | 46017f5725 | ||
|   | c20ce12f86 | ||
|   | 1e28db2396 | ||
|   | 5f3640c7fd | ||
|   | d65e5f6794 | ||
|   | e67d7bc0ea | ||
|   | 1139632f95 | ||
|   | b51a8c3f82 | ||
|   | 0d7256678e | ||
|   | eea33d07fd | ||
|   | f599337320 | ||
|   | 7df019db0e | ||
|   | 04f92bd688 | ||
|   | 505ecf6c1f | ||
|   | c9ec08704e | ||
|   | 6a3039f7b7 | ||
|   | 868c8fffb3 | ||
|   | faed3b438e | ||
|   | 6c982629ea | ||
|   | 110bbbc7dc | ||
|   | 4ad0345f20 | ||
|   | 9d84214462 | ||
|   | 3f199c7113 | ||
|   | e9417fb741 | ||
|   | ee74df6823 | ||
|   | 26630bae81 | ||
|   | 9bde9edcf6 | ||
|   | a12f07c42b | ||
|   | e7334c4fb0 | ||
|   | 38f9d1e764 | ||
|   | 2dfed75402 | ||
|   | 0c12e80106 | ||
|   | b7522f69e7 | ||
|   | 24705a7e39 | ||
|   | 8add8025a0 | ||
|   | 32fa79d928 | ||
|   | 534be6ff25 | ||
|   | f684c07567 | ||
|   | 788ae2f6ca | ||
|   | 572000f868 | ||
|   | 57f5df2d22 | ||
|   | b2a67ba5ca | ||
|   | d78e15cc1a | ||
|   | ceab34f5f3 | ||
|   | 3a62625bbc | ||
|   | ad6844ac4a | ||
|   | a8c252a613 | ||
|   | 1d39f785f1 | ||
|   | 4b8b29b862 | ||
|   | 0d148bd23b | ||
|   | ebedb81e3f | ||
|   | d195406fdc | ||
|   | 5173ed37f9 | ||
|   | 825551d64f | ||
|   | 449761bada | ||
|   | 5859df389f | ||
|   | 562b02310f | ||
|   | 65ed702d87 | ||
|   | c559a9843f | ||
|   | 88c3957085 | ||
|   | 01778e11dc | ||
|   | 9d9e8a3c4e | ||
|   | ed3e035ad6 | ||
|   | 07f885fea8 | ||
|   | 2cc98226ca | ||
|   | 8a6f73c5ff | ||
|   | 00e3453ce1 | ||
|   | 16646dd77a | ||
|   | 1f39d1fe26 | ||
|   | e8f3c587c9 | ||
|   | 4b43745e7c | ||
|   | 9db2f60053 | ||
|   | 4610d8dfe3 | ||
|   | fa296efdf6 | ||
|   | d9d98f84bf | ||
|   | 7c3143b8e5 | ||
|   | 387fcd5c5d | ||
|   | ebc6437977 | ||
|   | dbc23b5d20 | ||
|   | 843f1aed4f | ||
|   | e42938cad6 | ||
|   | 2a41f6c383 | ||
|   | 671d21a2c1 | ||
|   | 515692d7a6 | ||
|   | 00d28826b9 | ||
|   | 5b38f76254 | ||
|   | ca7dbd6010 | ||
|   | 133644e5a9 | ||
|   | 04d60426c7 | ||
|   | 8282bbd07c | ||
|   | 7190bd00c9 | ||
|   | 44b9539818 | ||
|   | b2ed4c9508 | ||
|   | c7b5c8b19e | 
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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
									
								
								.dockleignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.dockleignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | DKL-DI-0005 | ||||||
|  | DKL-DI-0006 | ||||||
|  | DKL-LI-0003 | ||||||
							
								
								
									
										2
									
								
								.github/workflows/docker-develop.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-develop.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
								
								.github/workflows/dockle.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/dockle.yml
									
									
									
									
										vendored
									
									
										Normal 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}" | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -32,6 +32,7 @@ coverage | |||||||
| !/.config/example.yml | !/.config/example.yml | ||||||
| !/.config/docker_example.yml | !/.config/docker_example.yml | ||||||
| !/.config/docker_example.env | !/.config/docker_example.env | ||||||
|  | docker-compose.yml | ||||||
|  |  | ||||||
| # misskey | # misskey | ||||||
| /build | /build | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -8,6 +8,84 @@ | |||||||
|  |  | ||||||
| You should also include the user name that made the change. | You should also include the user name that made the change. | ||||||
| --> | --> | ||||||
|  | ## 13.5.0 (2023/02/08) | ||||||
|  |  | ||||||
|  | ### Changes | ||||||
|  | - perf(client): do not render custom emojis in user names | ||||||
|  |  | ||||||
|  | ### Improvements | ||||||
|  | - Client: disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする | ||||||
|  | - enhance(client): tweak medialist style | ||||||
|  |  | ||||||
|  | ### Bugfixes | ||||||
|  | - fix docker health check | ||||||
|  | - Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正 | ||||||
|  | - fix(mfm): default degree not used in rotate | ||||||
|  | - fix(server): validate urls from ap to improve security | ||||||
|  |  | ||||||
|  | ## 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) | ||||||
|  | ### Changes | ||||||
|  | - docker-compose.ymlをdocker-compose.yml.exampleにしました。docker-compose.ymlとしてコピーしてから使用してください。 | ||||||
|  |  | ||||||
|  | ### Improvements | ||||||
|  | - 絵文字ピッカーのパフォーマンスを改善 | ||||||
|  | - AiScriptを0.12.4に更新 | ||||||
|  |  | ||||||
|  | ### Bugfixes | ||||||
|  | - Server: リレーと通信できない問題を修正 | ||||||
|  | - Client: classicモード使用時にwindowサイズによってdefaultに変更された後に、windowサイズが元に戻ったらclassicに戻すように修正 #9669 | ||||||
|  | - Client: Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう問題を修正 | ||||||
|  |  | ||||||
| ## 13.2.4 (2023/01/27) | ## 13.2.4 (2023/01/27) | ||||||
| ### Improvements | ### Improvements | ||||||
|   | |||||||
| @@ -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下に移してください | ||||||
|   | |||||||
| @@ -29,6 +29,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 | ||||||
|  |  | ||||||
| @@ -41,10 +42,12 @@ 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 -y --no-install-recommends \ | 	&& apt-get install -y --no-install-recommends \ | ||||||
| 	ffmpeg tini \ | 	ffmpeg tini curl \ | ||||||
| 	&& 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 | ||||||
| @@ -58,5 +61,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"] | ||||||
|   | |||||||
| @@ -24,6 +24,8 @@ | |||||||
| 	 | 	 | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | [](https://codecov.io/gh/misskey-dev/misskey) | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div> | <div> | ||||||
|   | |||||||
| @@ -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
									
								
								healthcheck.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								healthcheck.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}') | ||||||
|  | curl -s -S -o /dev/null "http://localhost:${PORT}" | ||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..." | |||||||
| ok: "OK" | ok: "OK" | ||||||
| gotIt: "Saya mengerti" | gotIt: "Saya mengerti" | ||||||
| cancel: "Batalkan" | cancel: "Batalkan" | ||||||
|  | noThankYou: "Tidak sekarang." | ||||||
| enterUsername: "Masukkan nama pengguna" | enterUsername: "Masukkan nama pengguna" | ||||||
| renotedBy: "direnote oleh {user}" | renotedBy: "direnote oleh {user}" | ||||||
| noNotes: "Tidak ada catatan" | noNotes: "Tidak ada catatan" | ||||||
| @@ -206,6 +207,7 @@ done: "Selesai" | |||||||
| processing: "Memproses" | processing: "Memproses" | ||||||
| preview: "Pratinjau" | preview: "Pratinjau" | ||||||
| default: "Bawaan" | default: "Bawaan" | ||||||
|  | defaultValueIs: "Bawaan: {value}" | ||||||
| noCustomEmojis: "Tidak ada emoji kustom" | noCustomEmojis: "Tidak ada emoji kustom" | ||||||
| noJobs: "Tidak ada kerja" | noJobs: "Tidak ada kerja" | ||||||
| federating: "memfederasi" | federating: "memfederasi" | ||||||
| @@ -349,6 +351,8 @@ recaptcha: "reCAPTCHA" | |||||||
| enableRecaptcha: "Nyalakan reCAPTCHA" | enableRecaptcha: "Nyalakan reCAPTCHA" | ||||||
| recaptchaSiteKey: "Site key" | recaptchaSiteKey: "Site key" | ||||||
| recaptchaSecretKey: "Secret Key" | recaptchaSecretKey: "Secret Key" | ||||||
|  | turnstile: "Turnstile" | ||||||
|  | enableTurnstile: "Nyalakan Turnstile" | ||||||
| turnstileSiteKey: "Site key" | turnstileSiteKey: "Site key" | ||||||
| turnstileSecretKey: "Secret Key" | turnstileSecretKey: "Secret Key" | ||||||
| avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal." | avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal." | ||||||
| @@ -454,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna" | |||||||
| groupInvited: "Telah diundang ke grup" | groupInvited: "Telah diundang ke grup" | ||||||
| aboutX: "Tentang {x}" | aboutX: "Tentang {x}" | ||||||
| emojiStyle: "Gaya emoji" | emojiStyle: "Gaya emoji" | ||||||
|  | native: "Native" | ||||||
| disableDrawer: "Jangan gunakan menu bergaya laci" | disableDrawer: "Jangan gunakan menu bergaya laci" | ||||||
| youHaveNoGroups: "Kamu tidak memiliki grup" | youHaveNoGroups: "Kamu tidak memiliki grup" | ||||||
| joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." | joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." | ||||||
| @@ -857,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui" | |||||||
| cropImage: "potong gambar" | cropImage: "potong gambar" | ||||||
| cropImageAsk: "Ingin memotong gambar?" | cropImageAsk: "Ingin memotong gambar?" | ||||||
| file: "Berkas" | file: "Berkas" | ||||||
|  | recentNHours: "{n} jam terakhir" | ||||||
|  | recentNDays: "{n} hari terakhir" | ||||||
| noEmailServerWarning: "Mail Server tidak disetel." | noEmailServerWarning: "Mail Server tidak disetel." | ||||||
|  | thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan." | ||||||
| recommended: "Disarankan" | recommended: "Disarankan" | ||||||
| check: "Cek" | check: "Cek" | ||||||
|  | driveCapOverrideLabel: "Ubah kapasitas drive untuk user ini" | ||||||
|  | driveCapOverrideCaption: "Setel ulang kapasitas ke bawaan dengan memasukkan nilai 0 atau lebih rendah." | ||||||
|  | requireAdminForView: "Kamu harus login dengan akun administrator untuk melihat ini." | ||||||
|  | isSystemAccount: "Akun yang dibuat dan otomatis dioperasikan oleh sistem." | ||||||
|  | typeToConfirm: "Mohon masukkan {x} untuk mengonfirmasi" | ||||||
| deleteAccount: "Hapus Akun" | deleteAccount: "Hapus Akun" | ||||||
|  | document: "Dokumen" | ||||||
|  | numberOfPageCache: "Jumlah halaman ditembolokkan" | ||||||
|  | numberOfPageCacheDescription: "Menaikkan jumlah ini akan meningkatkan kenyamanan untuk pengguna, namun dapat menyebabkan lonjakan beban pada peladen dan juga memori yang digunakan." | ||||||
| logoutConfirm: "Anda yakin ingin keluar?" | logoutConfirm: "Anda yakin ingin keluar?" | ||||||
| lastActiveDate: "Terakhir digunakan" | lastActiveDate: "Terakhir digunakan" | ||||||
| statusbar: "Bilah status" | statusbar: "Bilah status" | ||||||
| @@ -870,20 +886,189 @@ colored: "Diwarnai" | |||||||
| refreshInterval: "Jeda pembaharuan" | refreshInterval: "Jeda pembaharuan" | ||||||
| label: "Label" | label: "Label" | ||||||
| type: "Tipe" | type: "Tipe" | ||||||
|  | speed: "Kecepatan" | ||||||
|  | slow: "Lambat" | ||||||
|  | fast: "Cepat" | ||||||
|  | sensitiveMediaDetection: "Deteksi media NSFW" | ||||||
| localOnly: "Hanya lokal" | localOnly: "Hanya lokal" | ||||||
|  | remoteOnly: "Hanya remot" | ||||||
|  | failedToUpload: "Gagal mengunggah" | ||||||
|  | cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW." | ||||||
|  | cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive." | ||||||
|  | beta: "Beta" | ||||||
|  | enableAutoSensitive: "Penandaan NSFW otomatis" | ||||||
|  | enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen." | ||||||
|  | activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi." | ||||||
|  | navbar: "Bilah navigasi" | ||||||
| shuffle: "Acak" | shuffle: "Acak" | ||||||
| account: "Akun" | account: "Akun" | ||||||
|  | move: "Pindah" | ||||||
|  | pushNotification: "Pemberitahuan push" | ||||||
|  | subscribePushNotification: "Nyalakan pemberitahuan push" | ||||||
|  | unsubscribePushNotification: "Matikan pemberitahuan push" | ||||||
|  | pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan" | ||||||
|  | pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push" | ||||||
|  | sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca" | ||||||
|  | sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu." | ||||||
|  | windowMaximize: "Maksimalkan" | ||||||
|  | windowRestore: "Kembalikan" | ||||||
|  | caption: "Keterangan" | ||||||
|  | loggedInAsBot: "Sedang login sebagai bot" | ||||||
|  | tools: "Alat" | ||||||
|  | cannotLoad: "Tidak dapat memuat" | ||||||
|  | numberOfProfileView: "tayang profil" | ||||||
| like: "Suka" | like: "Suka" | ||||||
| unlike: "Tidak Suka" | unlike: "Tidak Suka" | ||||||
| numberOfLikes: "Jumlah yang disukai" | numberOfLikes: "Jumlah yang disukai" | ||||||
| show: "Tampilkan" | show: "Tampilkan" | ||||||
|  | neverShow: "Jangan tampilkan lagi" | ||||||
|  | remindMeLater: "Mungkin nanti" | ||||||
|  | didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?" | ||||||
|  | pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!" | ||||||
|  | roles: "Peran" | ||||||
|  | role: "Peran" | ||||||
| color: "Warna" | color: "Warna" | ||||||
|  | _achievements: | ||||||
|  |   _types: | ||||||
|  |     _login7: | ||||||
|  |       description: "Login selama 7 hari" | ||||||
|  |       flavor: "Sudah mulai terbiasa?" | ||||||
|  |     _login15: | ||||||
|  |       title: "Pemula III" | ||||||
|  |       description: "Login selama 15 hari" | ||||||
|  |     _login30: | ||||||
|  |       title: "Misskist I" | ||||||
|  |       description: "Login selama 30 hari" | ||||||
|  |     _login60: | ||||||
|  |       title: "Misskist II" | ||||||
|  |       description: "Login selama 60 hari" | ||||||
|  |     _login100: | ||||||
|  |       title: "Misskist III" | ||||||
|  |       description: "Login selama 100 hari" | ||||||
|  |       flavor: "Violent Misskist" | ||||||
|  |     _login200: | ||||||
|  |       title: "Reguler I" | ||||||
|  |       description: "Login selama 200 hari" | ||||||
|  |     _login300: | ||||||
|  |       title: "Reguler II" | ||||||
|  |       description: "Login selama 300 hari" | ||||||
|  |     _login400: | ||||||
|  |       title: "Reguler III" | ||||||
|  |       description: "Login selama 400 hari" | ||||||
|  |     _login500: | ||||||
|  |       title: "Veteran I" | ||||||
|  |       description: "Login selama 500 hari" | ||||||
|  |       flavor: "Kawanku, aku suka catatan." | ||||||
|  |     _login600: | ||||||
|  |       title: "Veteran II" | ||||||
|  |       description: "Login selama 600 hari" | ||||||
|  |     _login700: | ||||||
|  |       title: "Veteran III" | ||||||
|  |       description: "Login selama 700 hari" | ||||||
|  |     _login800: | ||||||
|  |       title: "Sepuh Catatan I" | ||||||
|  |       description: "Login selama 800 hari" | ||||||
|  |     _login900: | ||||||
|  |       title: "Sepuh Catatan II" | ||||||
|  |       description: "Login selama 900 hari" | ||||||
|  |     _login1000: | ||||||
|  |       title: "Sepuh Catatan III" | ||||||
|  |       description: "Login selama 1000 hari" | ||||||
|  |       flavor: "Terima kasih telah menggunakan Misskey!" | ||||||
|  |     _noteClipped1: | ||||||
|  |       title: "Harus... Ngeklip..." | ||||||
|  |       description: "Klip catatan pertamamu" | ||||||
|  |     _noteFavorited1: | ||||||
|  |       title: "Pengamat Bintang" | ||||||
|  |       description: "Favoritkan catatan pertamamu" | ||||||
|  |     _myNoteFavorited1: | ||||||
|  |       title: "Pencari Bintang" | ||||||
|  |       description: "Minta orang lain memfavoritkan salah satu catatanmu" | ||||||
|  |     _profileFilled: | ||||||
|  |       title: "Siap Sedia" | ||||||
|  |       description: "Atur profil kamu" | ||||||
|  |     _markedAsCat: | ||||||
|  |       title: "Aku Seekor Kucing" | ||||||
|  |       description: "Tandai akunmu sebagai kucing" | ||||||
|  |       flavor: "Aku beri kamu nama nanti" | ||||||
|  |     _following1: | ||||||
|  |       title: "Ikuti pengguna lain pertamamu" | ||||||
|  |       description: "Ikuti pengguna" | ||||||
|  |     _following10: | ||||||
|  |       title: "Terusin... terusin..." | ||||||
|  |       description: "Ikuti 10 pengguna lain" | ||||||
|  |     _following50: | ||||||
|  |       title: "Banyak teman" | ||||||
|  |       description: "Ikuti 50 pengguna lain" | ||||||
|  |     _following100: | ||||||
|  |       title: "100 Teman" | ||||||
|  |       description: "Ikuti 100 pengguna lain" | ||||||
|  |     _clickedClickHere: | ||||||
|  |       description: "Kamu telah mengeklik disini" | ||||||
|  |     _justPlainLucky: | ||||||
|  |       title: "Lagi Beruntung" | ||||||
|  |       description: "Mendapatkan kesempatan dengan kemungkinan 0.01% setiap 10 detik" | ||||||
|  |     _setNameToSyuilo: | ||||||
|  |       title: "God Complex" | ||||||
|  |       description: "Atur namamu jadi \"syuilo\"" | ||||||
|  |     _passedSinceAccountCreated1: | ||||||
|  |       title: "Perayaan Satu Tahun" | ||||||
|  |       description: "Satu tahun telah lewat sejak akunmu dibuat" | ||||||
|  |     _passedSinceAccountCreated2: | ||||||
|  |       title: "Perayaan Dua Tahun" | ||||||
|  |       description: "Dua tahun telah lewat sejak akunmu dibuat" | ||||||
|  |     _passedSinceAccountCreated3: | ||||||
|  |       title: "Perayaan Tiga Tahun" | ||||||
|  |       description: "Tiga tahun telah lewat sejak akunmu dibuat" | ||||||
|  |     _loggedInOnBirthday: | ||||||
|  |       title: "Selamat Ulang Tahun" | ||||||
|  |       description: "Login di hari ulang tahunmu" | ||||||
|  |     _loggedInOnNewYearsDay: | ||||||
|  |       title: "Selamat Tahun Baru!" | ||||||
|  |       description: "Login di hari pertama tahun baru" | ||||||
|  |     _cookieClicked: | ||||||
|  |       title: "Permainan dimana kamu mengeklik kue" | ||||||
|  |       description: "Mengeklik kue" | ||||||
|  |       flavor: "Tunggu, apakah kamu sedang berada di website yang benar?" | ||||||
|  |     _brainDiver: | ||||||
|  |       title: "Brain Diver" | ||||||
|  |       description: "Posting tautan mengenai Brain Diver" | ||||||
|  |       flavor: "Misskey-Misskey La-Tu-Ma" | ||||||
| _role: | _role: | ||||||
|  |   new: "Buat peran" | ||||||
|  |   edit: "Sunting peran" | ||||||
|  |   name: "Nama peran" | ||||||
|  |   description: "Deskripsi peran" | ||||||
|  |   permission: "Perijinan peran" | ||||||
|  |   descriptionOfPermission: "<b>Moderator</b> dapat melakukan operasi moderasi dasar.\n<b>Administrator</b> dapat mengubah seluruh pengaturan instansi." | ||||||
|  |   assignTarget: "Tipe tugas" | ||||||
|  |   descriptionOfAssignTarget: "<b>Manual</b> untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\n<b>Kondisional</b> untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan." | ||||||
|  |   manual: "Manual" | ||||||
|  |   conditional: "Kondisional" | ||||||
|  |   condition: "Kondisi" | ||||||
|  |   isConditionalRole: "Ini adalah peran kondisional" | ||||||
|  |   isPublic: "Publikkan Peran" | ||||||
|  |   descriptionOfIsPublic: "Siapapun dapat melihat daftar pengguna yang ditugaskan pada peran ini. Tambahan juga peran ini akan ditampilkan ke dalam profil pengguna tentang peran yang ditugaskan." | ||||||
|  |   options: "Opsi peran" | ||||||
|  |   policies: "Kebijakan" | ||||||
|  |   baseRole: "Templat peran" | ||||||
|  |   useBaseValue: "Gunakan nilai templat peran" | ||||||
|  |   chooseRoleToAssign: "Pilih peran yang ditugaskan" | ||||||
|  |   canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini" | ||||||
|  |   descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini." | ||||||
|   priority: "Prioritas" |   priority: "Prioritas" | ||||||
|   _priority: |   _priority: | ||||||
|     low: "Rendah" |     low: "Rendah" | ||||||
|     middle: "Sedang" |     middle: "Sedang" | ||||||
|     high: "Tinggi" |     high: "Tinggi" | ||||||
|  |   _options: | ||||||
|  |     gtlAvailable: "Dapat melihat linimasa global" | ||||||
|  |     ltlAvailable: "Dapat melihat linimasa lokal" | ||||||
|  |     canPublicNote: "Dapat mengirim catatan publik" | ||||||
|  |     canInvite: "Dapat membuat kode undangan instansi" | ||||||
|  |     canManageCustomEmojis: "Dapat mengelola Emoji kustom" | ||||||
|  |     driveCapacity: "Kapasitas Drive" | ||||||
|  |     pinMax: "Jumlah maksimal catatan yang disematkan" | ||||||
| _emailUnavailable: | _emailUnavailable: | ||||||
|   used: "Alamat surel ini telah digunakan" |   used: "Alamat surel ini telah digunakan" | ||||||
|   format: "Format tidak valid." |   format: "Format tidak valid." | ||||||
| @@ -1167,6 +1352,7 @@ _tutorial: | |||||||
|   step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey." |   step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey." | ||||||
|   step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}." |   step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}." | ||||||
|   step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀" |   step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀" | ||||||
|  |   step8_3: "Kamu dapat mengganti pengaturan ini nanti." | ||||||
| _2fa: | _2fa: | ||||||
|   alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." |   alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." | ||||||
|   registerDevice: "Daftarkan perangkat baru" |   registerDevice: "Daftarkan perangkat baru" | ||||||
| @@ -1241,10 +1427,13 @@ _widgets: | |||||||
|   trends: "Tren" |   trends: "Tren" | ||||||
|   clock: "Jam" |   clock: "Jam" | ||||||
|   rss: "Pembaca RSS" |   rss: "Pembaca RSS" | ||||||
|  |   rssTicker: "RSS-Ticker" | ||||||
|   activity: "Aktivitas" |   activity: "Aktivitas" | ||||||
|   photos: "Foto" |   photos: "Foto" | ||||||
|   digitalClock: "Jam digital" |   digitalClock: "Jam digital" | ||||||
|  |   unixClock: "Jam UNIX" | ||||||
|   federation: "Federasi" |   federation: "Federasi" | ||||||
|  |   instanceCloud: "Instansi awan" | ||||||
|   postForm: "Buat catatan" |   postForm: "Buat catatan" | ||||||
|   slideshow: "Slideshow" |   slideshow: "Slideshow" | ||||||
|   button: "Tombol" |   button: "Tombol" | ||||||
| @@ -1254,8 +1443,10 @@ _widgets: | |||||||
|   aiscript: "Konsol AiScript" |   aiscript: "Konsol AiScript" | ||||||
|   aiscriptApp: "Aplikasi AiScript" |   aiscriptApp: "Aplikasi AiScript" | ||||||
|   aichan: "Ai" |   aichan: "Ai" | ||||||
|  |   userList: "Daftar pengguna" | ||||||
|   _userList: |   _userList: | ||||||
|     chooseList: "Pilih daftar" |     chooseList: "Pilih daftar" | ||||||
|  |   clicker: "Pengeklik" | ||||||
| _cw: | _cw: | ||||||
|   hide: "Sembunyikan" |   hide: "Sembunyikan" | ||||||
|   show: "Lihat konten" |   show: "Lihat konten" | ||||||
| @@ -1319,6 +1510,7 @@ _profile: | |||||||
|   changeBanner: "Ubah header" |   changeBanner: "Ubah header" | ||||||
| _exportOrImport: | _exportOrImport: | ||||||
|   allNotes: "Semua catatan" |   allNotes: "Semua catatan" | ||||||
|  |   favoritedNotes: "Catatan favorit" | ||||||
|   followingList: "Ikuti" |   followingList: "Ikuti" | ||||||
|   muteList: "Bisukan" |   muteList: "Bisukan" | ||||||
|   blockingList: "Blokir" |   blockingList: "Blokir" | ||||||
| @@ -1437,7 +1629,9 @@ _notification: | |||||||
|   yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" |   yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" | ||||||
|   youWereInvitedToGroup: "Telah diundang ke grup" |   youWereInvitedToGroup: "Telah diundang ke grup" | ||||||
|   pollEnded: "Hasil Kuesioner telah keluar" |   pollEnded: "Hasil Kuesioner telah keluar" | ||||||
|  |   unreadAntennaNote: "Antena {name}" | ||||||
|   emptyPushNotificationMessage: "Pembaruan notifikasi dorong" |   emptyPushNotificationMessage: "Pembaruan notifikasi dorong" | ||||||
|  |   achievementEarned: "Pencapaian didapatkan" | ||||||
|   _types: |   _types: | ||||||
|     all: "Semua" |     all: "Semua" | ||||||
|     follow: "Ikuti" |     follow: "Ikuti" | ||||||
| @@ -1459,6 +1653,7 @@ _deck: | |||||||
|   alwaysShowMainColumn: "Selalu tampilkan kolom utama" |   alwaysShowMainColumn: "Selalu tampilkan kolom utama" | ||||||
|   columnAlign: "Luruskan kolom" |   columnAlign: "Luruskan kolom" | ||||||
|   addColumn: "Tambahkan kolom" |   addColumn: "Tambahkan kolom" | ||||||
|  |   configureColumn: "Atur kolom" | ||||||
|   swapLeft: "Pindah ke kiri" |   swapLeft: "Pindah ke kiri" | ||||||
|   swapRight: "Pindah ke kanan" |   swapRight: "Pindah ke kanan" | ||||||
|   swapUp: "Pindah ke atas" |   swapUp: "Pindah ke atas" | ||||||
| @@ -1466,6 +1661,11 @@ _deck: | |||||||
|   stackLeft: "Tumpukkan di kolom kiri" |   stackLeft: "Tumpukkan di kolom kiri" | ||||||
|   popRight: "Keluarkan di kanan" |   popRight: "Keluarkan di kanan" | ||||||
|   profile: "Profil" |   profile: "Profil" | ||||||
|  |   newProfile: "Profil baru" | ||||||
|  |   deleteProfile: "Hapus profil" | ||||||
|  |   introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!" | ||||||
|  |   introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau." | ||||||
|  |   widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit." | ||||||
|   _columns: |   _columns: | ||||||
|     main: "Utama" |     main: "Utama" | ||||||
|     widgets: "Widget" |     widgets: "Widget" | ||||||
|   | |||||||
| @@ -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', | ||||||
|   | |||||||
| @@ -1044,7 +1044,7 @@ _achievements: | |||||||
|       flavor: "Grazie per aver usato Misskey!" |       flavor: "Grazie per aver usato Misskey!" | ||||||
|     _noteClipped1: |     _noteClipped1: | ||||||
|       title: "Devo clippare!" |       title: "Devo clippare!" | ||||||
|       description: "Ho raccolto in Clip la prima Nota" |       description: "Hai raccolto la tua prima Nota in una Clip" | ||||||
|     _noteFavorited1: |     _noteFavorited1: | ||||||
|       title: "Guarda le stelle" |       title: "Guarda le stelle" | ||||||
|       description: "Aggiungi una Nota ai preferiti per la prima volta" |       description: "Aggiungi una Nota ai preferiti per la prima volta" | ||||||
| @@ -1080,7 +1080,7 @@ _achievements: | |||||||
|       title: "Follow me!" |       title: "Follow me!" | ||||||
|       description: "Hai ottenuto 10 profili Follower" |       description: "Hai ottenuto 10 profili Follower" | ||||||
|     _followers50: |     _followers50: | ||||||
|       title: "Follower a frotte" |       title: "Un gregge di Follower" | ||||||
|       description: "Hai ottenuto 50 Follower" |       description: "Hai ottenuto 50 Follower" | ||||||
|     _followers100: |     _followers100: | ||||||
|       title: "Popolare" |       title: "Popolare" | ||||||
| @@ -1108,7 +1108,7 @@ _achievements: | |||||||
|       title: "Caccia al tesoro" |       title: "Caccia al tesoro" | ||||||
|       description: "Hai trovato un tesoro nascosto" |       description: "Hai trovato un tesoro nascosto" | ||||||
|     _client30min: |     _client30min: | ||||||
|       title: "Piccola pausa" |       title: "Piccola grande pausa" | ||||||
|       description: "Hai passato più di 30 minuti su Misskey" |       description: "Hai passato più di 30 minuti su Misskey" | ||||||
|     _noteDeletedWithin1min: |     _noteDeletedWithin1min: | ||||||
|       title: "Ooops!" |       title: "Ooops!" | ||||||
| @@ -1134,7 +1134,7 @@ _achievements: | |||||||
|       title: "Hello, world!" |       title: "Hello, world!" | ||||||
|       description: "Hai scritto «Hello world» nel blocco appunti" |       description: "Hai scritto «Hello world» nel blocco appunti" | ||||||
|     _open3windows: |     _open3windows: | ||||||
|       title: "Finestrato" |       title: "Apri le finestre!" | ||||||
|       description: "Hai aperto almeno 3 finestre contemporaneamente" |       description: "Hai aperto almeno 3 finestre contemporaneamente" | ||||||
|     _driveFolderCircularReference: |     _driveFolderCircularReference: | ||||||
|       title: "Riferimento circolare" |       title: "Riferimento circolare" | ||||||
| @@ -1170,7 +1170,7 @@ _achievements: | |||||||
|     _cookieClicked: |     _cookieClicked: | ||||||
|       title: "Clicca il biscotto" |       title: "Clicca il biscotto" | ||||||
|       description: "Hai giocato a cliccare il cookie" |       description: "Hai giocato a cliccare il cookie" | ||||||
|       flavor: "Hai autorizzato i cookie?" |       flavor: "È il sito giusto?" | ||||||
|     _brainDiver: |     _brainDiver: | ||||||
|       title: "Brain Diver" |       title: "Brain Diver" | ||||||
|       description: "Pubblica un link a Brain Diver" |       description: "Pubblica un link a Brain Diver" | ||||||
| @@ -1195,6 +1195,9 @@ _role: | |||||||
|   baseRole: "Ruolo di base" |   baseRole: "Ruolo di base" | ||||||
|   useBaseValue: "Eredita dal ruolo base" |   useBaseValue: "Eredita dal ruolo base" | ||||||
|   chooseRoleToAssign: "Seleziona il ruolo da assegnare" |   chooseRoleToAssign: "Seleziona il ruolo da assegnare" | ||||||
|  |   iconUrl: "URL dell'icona" | ||||||
|  |   asBadge: "Mostra come badge" | ||||||
|  |   descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo." | ||||||
|   canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" |   canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo" | ||||||
|   descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." |   descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori." | ||||||
|   priority: "Priorità" |   priority: "Priorità" | ||||||
|   | |||||||
| @@ -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: "優先度" | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								locales/lo-LA.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								locales/lo-LA.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | --- | ||||||
|  | _lang_: "ພາສາລາວ" | ||||||
|  | headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ" | ||||||
|  | introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀" | ||||||
|  | poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"Misskey instance\")" | ||||||
|  | monthAndDay: "{ເດືອນ}/{ມື້}" | ||||||
|  | search: "ຄົ້ນຫາ" | ||||||
|  | notifications: "ການແຈ້ງເຕືອນ" | ||||||
|  | username: "ຊື່ຜູ້ໃຊ້" | ||||||
|  | password: "ລະຫັດຜ່ານ" | ||||||
|  | forgotPassword: "ລືມລະຫັດຜ່ານ" | ||||||
|  | fetchingAsApObject: "ກຳລັງດຶງຂໍ້ມູນຈາກ fediverse..." | ||||||
|  | ok: "ຕົກລົງ" | ||||||
|  | gotIt: "ເຂົ້າໃຈແລ້ວ!" | ||||||
|  | cancel: "ຍົກເລີກ" | ||||||
|  | noThankYou: "ບໍ່ແມ່ນຕອນນີ້" | ||||||
|  | enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້" | ||||||
|  | renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}" | ||||||
|  | noNotes: "ບໍ່ມີຫມາຍເຫດ" | ||||||
|  | noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" | ||||||
|  | instance: "ອີນສະແຕນ" | ||||||
|  | settings: "ກຳນົດຄ່າ" | ||||||
|  | basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" | ||||||
|  | otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" | ||||||
|  | openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" | ||||||
|  | profile: "ໂພຼຟາຍ" | ||||||
|  | timeline: "ເສັ້ນກຳນົດເວລາ" | ||||||
|  | noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ" | ||||||
|  | login: "ເຂົ້າສູ່ລະບົບ" | ||||||
|  | loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..." | ||||||
|  | logout: "ອອກຈາກລະບົບ" | ||||||
|  | signup: "ລົງທະບຽນ" | ||||||
|  | uploading: "ການອັບໂຫຼດ..." | ||||||
|  | save: "ບັນທຶກ" | ||||||
|  | users: "ຜູ້ໃຊ້ຕ່າງໆ" | ||||||
|  | addUser: "ເພີ່ມຜູ້ໃຊ້" | ||||||
|  | favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ" | ||||||
|  | favorites: "ລາຍການທີ່ມັກ" | ||||||
|  | unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ" | ||||||
|  | favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ" | ||||||
|  | alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ." | ||||||
|  | cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້." | ||||||
|  | pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" | ||||||
|  | unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌" | ||||||
|  | copyContent: "ຄັດລອກເນື້ອຫາ" | ||||||
|  | copyLink: "ສຳເນົາລິ້ງ" | ||||||
|  | delete: "ລຶບ" | ||||||
|  | deleteAndEdit: "ລົບແລະແກ້ໄຂ" | ||||||
|  | deleteAndEditConfirm: "ເຈົ້າແນ່ໃຈບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" | ||||||
|  | addToList: "ເພີ່ມໃສ່ລາຍຊື່" | ||||||
|  | sendMessage: "ສົ່ງຂໍ້ຄວາມ" | ||||||
|  | pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" | ||||||
|  | instances: "ອີນສະແຕນ" | ||||||
|  | remove: "ລຶບ" | ||||||
|  | smtpUser: "ຊື່ຜູ້ໃຊ້" | ||||||
|  | smtpPass: "ລະຫັດຜ່ານ" | ||||||
|  | user: "ຜູ້ໃຊ້ຕ່າງໆ" | ||||||
|  | searchByGoogle: "ຄົ້ນຫາ" | ||||||
|  | _mfm: | ||||||
|  |   search: "ຄົ້ນຫາ" | ||||||
|  | _sfx: | ||||||
|  |   notification: "ການແຈ້ງເຕືອນ" | ||||||
|  | _widgets: | ||||||
|  |   profile: "ໂພຼຟາຍ" | ||||||
|  |   notifications: "ການແຈ້ງເຕືອນ" | ||||||
|  |   timeline: "ເສັ້ນກຳນົດເວລາ" | ||||||
|  | _profile: | ||||||
|  |   username: "ຊື່ຜູ້ໃຊ້" | ||||||
|  | _deck: | ||||||
|  |   _columns: | ||||||
|  |     notifications: "ການແຈ້ງເຕືອນ" | ||||||
|  |     tl: "ເສັ້ນກຳນົດເວລາ" | ||||||
| @@ -22,7 +22,7 @@ instance: "Инстанс" | |||||||
| settings: "Настройки" | settings: "Настройки" | ||||||
| basicSettings: "Основные настройки" | basicSettings: "Основные настройки" | ||||||
| otherSettings: "Прочие настройки" | otherSettings: "Прочие настройки" | ||||||
| openInWindow: "Открывать в плавающих окнах" | openInWindow: "Открыть в плавающем окне" | ||||||
| profile: "Профиль" | profile: "Профиль" | ||||||
| timeline: "Лента" | timeline: "Лента" | ||||||
| noAccountDescription: "Пользователь ничего не написал про себя" | noAccountDescription: "Пользователь ничего не написал про себя" | ||||||
| @@ -273,7 +273,7 @@ light: "Светлый" | |||||||
| dark: "Тёмный" | dark: "Тёмный" | ||||||
| lightThemes: "Светлые темы" | lightThemes: "Светлые темы" | ||||||
| darkThemes: "Тёмные темы" | darkThemes: "Тёмные темы" | ||||||
| syncDeviceDarkMode: "Синхронизировать с темным режимом устройства" | syncDeviceDarkMode: "Синхронизировать с тёмной темой системы" | ||||||
| drive: "Диск" | drive: "Диск" | ||||||
| fileName: "Имя файла" | fileName: "Имя файла" | ||||||
| selectFile: "Выберите файл" | selectFile: "Выберите файл" | ||||||
| @@ -456,6 +456,7 @@ uiLanguage: "Язык интерфейса" | |||||||
| groupInvited: "Приглашение в группу" | groupInvited: "Приглашение в группу" | ||||||
| aboutX: "Описание {x}" | aboutX: "Описание {x}" | ||||||
| emojiStyle: "Стиль эмодзи" | emojiStyle: "Стиль эмодзи" | ||||||
|  | native: "Системные" | ||||||
| disableDrawer: "Не использовать выдвижные меню" | disableDrawer: "Не использовать выдвижные меню" | ||||||
| youHaveNoGroups: "У вас нет ни одной группы" | youHaveNoGroups: "У вас нет ни одной группы" | ||||||
| joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" | joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" | ||||||
| @@ -603,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS." | |||||||
| testEmail: "Проверка доставки электронной почты" | testEmail: "Проверка доставки электронной почты" | ||||||
| wordMute: "Скрытие слов" | wordMute: "Скрытие слов" | ||||||
| regexpError: "Ошибка в регулярном выражении" | regexpError: "Ошибка в регулярном выражении" | ||||||
|  | regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:" | ||||||
| instanceMute: "Глушение инстансов" | instanceMute: "Глушение инстансов" | ||||||
| userSaysSomething: "{name} что-то сообщает" | userSaysSomething: "{name} что-то сообщает" | ||||||
| makeActive: "Активировать" | makeActive: "Активировать" | ||||||
| @@ -804,7 +806,7 @@ translate: "Перевод" | |||||||
| translatedFrom: "Перевод. Язык оригинала — {x}" | translatedFrom: "Перевод. Язык оригинала — {x}" | ||||||
| accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" | accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" | ||||||
| usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." | usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." | ||||||
| aiChanMode: "ИИ режим" | aiChanMode: "Режим Ай" | ||||||
| keepCw: "Сохраняйте Предупреждения о содержимом" | keepCw: "Сохраняйте Предупреждения о содержимом" | ||||||
| pubSub: "Учётные записи Pub/Sub" | pubSub: "Учётные записи Pub/Sub" | ||||||
| lastCommunication: "Последнее сообщение" | lastCommunication: "Последнее сообщение" | ||||||
| @@ -821,8 +823,8 @@ manageAccounts: "Управление аккаунтом" | |||||||
| makeReactionsPublic: "Опубликовать список реакций" | makeReactionsPublic: "Опубликовать список реакций" | ||||||
| makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим." | makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим." | ||||||
| classic: "Классика" | classic: "Классика" | ||||||
| muteThread: "Заглушить цепочку" | muteThread: "Скрыть цепочку" | ||||||
| unmuteThread: "Отменить глушение цепочки" | unmuteThread: "Отменить сокрытие цепочки" | ||||||
| ffVisibility: "Видимость подписок и подписчиков" | ffVisibility: "Видимость подписок и подписчиков" | ||||||
| ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков." | ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков." | ||||||
| continueThread: "Показать следующие ответы" | continueThread: "Показать следующие ответы" | ||||||
| @@ -891,6 +893,7 @@ cannotUploadBecauseNoFreeSpace: "Файл не может быть загруж | |||||||
| beta: "Бета" | beta: "Бета" | ||||||
| enableAutoSensitive: "Автоматическое определение NSFW" | enableAutoSensitive: "Автоматическое определение NSFW" | ||||||
| enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта." | enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта." | ||||||
|  | activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса." | ||||||
| navbar: "Панель навигации" | navbar: "Панель навигации" | ||||||
| shuffle: "Перемешать" | shuffle: "Перемешать" | ||||||
| account: "Учётные записи" | account: "Учётные записи" | ||||||
| @@ -1096,6 +1099,9 @@ _achievements: | |||||||
|       title: "Я люблю Misskey" |       title: "Я люблю Misskey" | ||||||
|       description: "Написана заметка «I ❤ #Misskey»" |       description: "Написана заметка «I ❤ #Misskey»" | ||||||
|       flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков" |       flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков" | ||||||
|  |     _foundTreasure: | ||||||
|  |       title: "Охота за сокровищами" | ||||||
|  |       description: "Найдено спрятанное сокровище" | ||||||
|     _client30min: |     _client30min: | ||||||
|       title: "Перерыв на обед" |       title: "Перерыв на обед" | ||||||
|       description: "Прошло 30 минут с момента запуска клиента" |       description: "Прошло 30 минут с момента запуска клиента" | ||||||
| @@ -1116,6 +1122,9 @@ _achievements: | |||||||
|     _htl20npm: |     _htl20npm: | ||||||
|       title: "В потоке" |       title: "В потоке" | ||||||
|       description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)" |       description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)" | ||||||
|  |     _viewInstanceChart: | ||||||
|  |       title: "Аналитик" | ||||||
|  |       description: "Просмотрены статистические диаграммы инстанса" | ||||||
|     _outputHelloWorldOnScratchpad: |     _outputHelloWorldOnScratchpad: | ||||||
|       title: "Привет, мир!" |       title: "Привет, мир!" | ||||||
|       description: "Выведен текст «hello world» в Когтеточке" |       description: "Выведен текст «hello world» в Когтеточке" | ||||||
| @@ -1189,7 +1198,34 @@ _role: | |||||||
|     middle: "Средне" |     middle: "Средне" | ||||||
|     high: "Высокий" |     high: "Высокий" | ||||||
|   _options: |   _options: | ||||||
|  |     gtlAvailable: "Может просматривать глобальную ленту" | ||||||
|  |     ltlAvailable: "Может просматривать местную ленту" | ||||||
|  |     canPublicNote: "Может публиковать общедоступные заметки" | ||||||
|  |     canInvite: "Может создавать пригласительные коды" | ||||||
|     canManageCustomEmojis: "Управлять пользовательскими эмодзи" |     canManageCustomEmojis: "Управлять пользовательскими эмодзи" | ||||||
|  |     driveCapacity: "Доступное пространство на «диске»" | ||||||
|  |     pinMax: "Доступное количество закреплённых заметок" | ||||||
|  |     antennaMax: "Доступное количество антенн" | ||||||
|  |     wordMuteMax: "Доступное количество знаков в списке скрытия слов" | ||||||
|  |     clipMax: "Максимальное количество подборок" | ||||||
|  |     noteEachClipsMax: "Максимальное количество заметок в подборке" | ||||||
|  |     userListMax: "Максимальное количество списков аккаунтов" | ||||||
|  |     userEachUserListsMax: "Максимальное количество аккаунтов в списке" | ||||||
|  |     rateLimitFactor: "Ограничение активности" | ||||||
|  |     descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные" | ||||||
|  |     canHideAds: "Может скрыть рекламу" | ||||||
|  |   _condition: | ||||||
|  |     isLocal: "Местный" | ||||||
|  |     isRemote: "Неместный" | ||||||
|  |     createdLessThan: "Аккаунт младше, чем..." | ||||||
|  |     createdMoreThan: "Аккаунт старше, чем..." | ||||||
|  |     followersLessThanOrEq: "Количество подписчиков не превышает…" | ||||||
|  |     followersMoreThanOrEq: "Количество подписчиков не меньше чем…" | ||||||
|  |     followingLessThanOrEq: "Количество подписок не превышает…" | ||||||
|  |     followingMoreThanOrEq: "Количество подписок не меньше чем…" | ||||||
|  |     and: "Выполнено несколько условий:.." | ||||||
|  |     or: "Выполнено любое из условий:.." | ||||||
|  |     not: "Кроме тех, у кого…" | ||||||
| _sensitiveMediaDetection: | _sensitiveMediaDetection: | ||||||
|   description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." |   description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." | ||||||
|   setSensitiveFlagAutomatically: "Установить флаг NSFW" |   setSensitiveFlagAutomatically: "Установить флаг NSFW" | ||||||
| @@ -1237,10 +1273,23 @@ _plugin: | |||||||
|   installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." |   installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете." | ||||||
|   manage: "Управление расширениями" |   manage: "Управление расширениями" | ||||||
| _preferencesBackups: | _preferencesBackups: | ||||||
|   saveConfirm: "Сохранить бэкап как {name}?" |   list: "Существующие резервные копии" | ||||||
|   deleteConfirm: "Удалить резервную копию {name}?" |   saveNew: "Создать резервную копию" | ||||||
|   renameConfirm: "Переименовать резервную копию с \"{old}\" на \"{new}\"?" |   loadFile: "Прочесть из файла" | ||||||
|   noBackups: "Резервной копии не существует. Вы можете создать резервную копию в настройках на этом инстансе с помощью \"Создать новую резервную копию\"." |   apply: "Восстановить на это устройство" | ||||||
|  |   save: "Обновить из текущих настроек" | ||||||
|  |   inputName: "Введите название для резервной копии" | ||||||
|  |   cannotSave: "Сохранить не удалось" | ||||||
|  |   nameAlreadyExists: "Резервная копия под названием «{name}» уже существует. Придумайте другое." | ||||||
|  |   applyConfirm: "Правда хотите загрузить резервную копию «{name}» на это устройство? Этим будут потеряны текущие настройки." | ||||||
|  |   saveConfirm: "Сохранить резервную копию под названием «{name}»?" | ||||||
|  |   deleteConfirm: "Удалить резервную копию «{name}»?" | ||||||
|  |   renameConfirm: "Переименовать резервную копию «{old}» в «{new}»?" | ||||||
|  |   noBackups: "Здесь ещё нет резервных копий. Вы можете создать резервную копию настроек на этом сайте с помощью кнопки «Создать резервную копию»." | ||||||
|  |   createdAt: "Создана {date} в {time}" | ||||||
|  |   updatedAt: "Обновлена {date} в {time}" | ||||||
|  |   cannotLoad: "Загрузить не удалось" | ||||||
|  |   invalidFile: "Некорректный формат файла" | ||||||
| _registry: | _registry: | ||||||
|   scope: "Область" |   scope: "Область" | ||||||
|   key: "Ключ" |   key: "Ключ" | ||||||
| @@ -1324,6 +1373,8 @@ _mfm: | |||||||
|   sparkleDescription: "Добавляет эффект искрящихся частиц." |   sparkleDescription: "Добавляет эффект искрящихся частиц." | ||||||
|   rotate: "Повернуть" |   rotate: "Повернуть" | ||||||
|   rotateDescription: "Поворачивает на заданный угол." |   rotateDescription: "Поворачивает на заданный угол." | ||||||
|  |   plain: "Буквально" | ||||||
|  |   plainDescription: "MFM внутри отключается, и текст отображается как есть" | ||||||
| _instanceTicker: | _instanceTicker: | ||||||
|   none: "Не показывать" |   none: "Не показывать" | ||||||
|   remote: "Только для других сайтов" |   remote: "Только для других сайтов" | ||||||
| @@ -1353,12 +1404,14 @@ _wordMute: | |||||||
|   muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." |   muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)." | ||||||
|   softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты." |   softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты." | ||||||
|   hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся." |   hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся." | ||||||
|   soft: "Мягкий" |   soft: "Мягко" | ||||||
|   hard: "Жёсткий" |   hard: "Жёстко" | ||||||
|   mutedNotes: "Скрытые заметки" |   mutedNotes: "Скрытые заметки" | ||||||
| _instanceMute: | _instanceMute: | ||||||
|  |   instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться." | ||||||
|  |   instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке" | ||||||
|   title: "Скрывает заметки с заданных инстансов." |   title: "Скрывает заметки с заданных инстансов." | ||||||
|   heading: "Список заглушенных инстансов" |   heading: "Список скрытых инстансов" | ||||||
| _theme: | _theme: | ||||||
|   explore: "Обзор" |   explore: "Обзор" | ||||||
|   install: "Установить тему" |   install: "Установить тему" | ||||||
| @@ -1479,12 +1532,16 @@ _tutorial: | |||||||
|   step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!" |   step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!" | ||||||
|   step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»." |   step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»." | ||||||
|   step7_3: "Приятно вам провести время с Misskey🚀" |   step7_3: "Приятно вам провести время с Misskey🚀" | ||||||
|  |   step8_1: "Ах, да, не хотите ли включить push-уведомления?" | ||||||
|  |   step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey." | ||||||
|  |   step8_3: "Эту настройку вы всегда сможете поменять" | ||||||
| _2fa: | _2fa: | ||||||
|   alreadyRegistered: "Двухфакторная аутентификация уже настроена." |   alreadyRegistered: "Двухфакторная аутентификация уже настроена." | ||||||
|   registerDevice: "Зарегистрируйте ваше устройство" |   registerDevice: "Зарегистрируйте ваше устройство" | ||||||
|   registerKey: "Зарегистрировать ключ" |   registerKey: "Зарегистрировать ключ" | ||||||
|   step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." |   step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}." | ||||||
|   step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." |   step2: "Далее отсканируйте отображаемый QR-код при помощи приложения." | ||||||
|  |   step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):" | ||||||
|   step3: "И наконец, введите код, который покажет приложение." |   step3: "И наконец, введите код, который покажет приложение." | ||||||
|   step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." |   step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом." | ||||||
|   securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." |   securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве." | ||||||
| @@ -1501,7 +1558,7 @@ _permissions: | |||||||
|   "write:following": "Изменять спискок подписок" |   "write:following": "Изменять спискок подписок" | ||||||
|   "read:messaging": "Смотреть сообщения" |   "read:messaging": "Смотреть сообщения" | ||||||
|   "write:messaging": "Писать и удалять сообщения" |   "write:messaging": "Писать и удалять сообщения" | ||||||
|   "read:mutes": "Смотреть спискок скрытых пользователей" |   "read:mutes": "Смотреть список скрытых пользователей" | ||||||
|   "write:mutes": "Изменять список скрытых пользователей" |   "write:mutes": "Изменять список скрытых пользователей" | ||||||
|   "write:notes": "Писать и удалять заметки" |   "write:notes": "Писать и удалять заметки" | ||||||
|   "read:notifications": "Смотреть уведомления" |   "read:notifications": "Смотреть уведомления" | ||||||
| @@ -1552,10 +1609,13 @@ _widgets: | |||||||
|   trends: "Актуальное" |   trends: "Актуальное" | ||||||
|   clock: "Часы" |   clock: "Часы" | ||||||
|   rss: "Просмотр RSS" |   rss: "Просмотр RSS" | ||||||
|  |   rssTicker: "Бегущая строка RSS" | ||||||
|   activity: "Активность" |   activity: "Активность" | ||||||
|   photos: "Фото" |   photos: "Фото" | ||||||
|   digitalClock: "Цифровые часы" |   digitalClock: "Цифровые часы" | ||||||
|  |   unixClock: "Часы UNIX" | ||||||
|   federation: "Федерация" |   federation: "Федерация" | ||||||
|  |   instanceCloud: "Облако инстансов" | ||||||
|   postForm: "Форма отправки" |   postForm: "Форма отправки" | ||||||
|   slideshow: "Показ слайдов" |   slideshow: "Показ слайдов" | ||||||
|   button: "Кнопка" |   button: "Кнопка" | ||||||
| @@ -1563,9 +1623,12 @@ _widgets: | |||||||
|   jobQueue: "Очередь заданий" |   jobQueue: "Очередь заданий" | ||||||
|   serverMetric: "Показатели сервера" |   serverMetric: "Показатели сервера" | ||||||
|   aiscript: "Консоль AiScript" |   aiscript: "Консоль AiScript" | ||||||
|  |   aiscriptApp: "Приложение на AiScript" | ||||||
|   aichan: "Ай" |   aichan: "Ай" | ||||||
|  |   userList: "Список аккаунтов" | ||||||
|   _userList: |   _userList: | ||||||
|     chooseList: "Выберите список" |     chooseList: "Выберите список" | ||||||
|  |   clicker: "Счётчик щелчков" | ||||||
| _cw: | _cw: | ||||||
|   hide: "Спрятать" |   hide: "Спрятать" | ||||||
|   show: "Показать еще" |   show: "Показать еще" | ||||||
| @@ -1628,12 +1691,13 @@ _profile: | |||||||
|   changeAvatar: "Поменять аватар" |   changeAvatar: "Поменять аватар" | ||||||
|   changeBanner: "Поменять изображение в шапке" |   changeBanner: "Поменять изображение в шапке" | ||||||
| _exportOrImport: | _exportOrImport: | ||||||
|   allNotes: "Все записи\n" |   allNotes: "Все заметки\n" | ||||||
|  |   favoritedNotes: "Избранное" | ||||||
|   followingList: "Подписки" |   followingList: "Подписки" | ||||||
|   muteList: "Скрытые" |   muteList: "Скрытые" | ||||||
|   blockingList: "Заблокированные" |   blockingList: "Заблокированные" | ||||||
|   userLists: "Списки" |   userLists: "Списки" | ||||||
|   excludeMutingUsers: "За исключением заглушенных пользователей" |   excludeMutingUsers: "За исключением скрытых пользователей" | ||||||
|   excludeInactiveUsers: "Без неактивных учётных записей" |   excludeInactiveUsers: "Без неактивных учётных записей" | ||||||
| _charts: | _charts: | ||||||
|   federation: "Федерация" |   federation: "Федерация" | ||||||
| @@ -1737,6 +1801,8 @@ _notification: | |||||||
|   youReceivedFollowRequest: "У вас новый запрос на подписку." |   youReceivedFollowRequest: "У вас новый запрос на подписку." | ||||||
|   yourFollowRequestAccepted: "Ваш запрос на подписку одобрен." |   yourFollowRequestAccepted: "Ваш запрос на подписку одобрен." | ||||||
|   youWereInvitedToGroup: "Вы приглашены в группу." |   youWereInvitedToGroup: "Вы приглашены в группу." | ||||||
|  |   pollEnded: "Подведены окончательные итоги опроса" | ||||||
|  |   emptyPushNotificationMessage: "Обновлены push-уведомления" | ||||||
|   achievementEarned: "Получено достижение" |   achievementEarned: "Получено достижение" | ||||||
|   _types: |   _types: | ||||||
|     all: "Все" |     all: "Все" | ||||||
| @@ -1746,11 +1812,13 @@ _notification: | |||||||
|     renote: "Репосты" |     renote: "Репосты" | ||||||
|     quote: "Цитаты" |     quote: "Цитаты" | ||||||
|     reaction: "Реакции" |     reaction: "Реакции" | ||||||
|  |     pollEnded: "Окончания опросов" | ||||||
|     receiveFollowRequest: "Получен запрос на подписку" |     receiveFollowRequest: "Получен запрос на подписку" | ||||||
|     followRequestAccepted: "Запрос на подписку одобрен" |     followRequestAccepted: "Запрос на подписку одобрен" | ||||||
|     groupInvited: "Приглашение в группы" |     groupInvited: "Приглашение в группы" | ||||||
|     app: "Уведомления из приложений" |     app: "Уведомления из приложений" | ||||||
|   _actions: |   _actions: | ||||||
|  |     followBack: "отвечает взаимной подпиской" | ||||||
|     reply: "Ответить" |     reply: "Ответить" | ||||||
|     renote: "Репост" |     renote: "Репост" | ||||||
| _deck: | _deck: | ||||||
| @@ -1764,7 +1832,12 @@ _deck: | |||||||
|   swapDown: "Переставить ниже" |   swapDown: "Переставить ниже" | ||||||
|   stackLeft: "В столбик влево" |   stackLeft: "В столбик влево" | ||||||
|   popRight: "Из столбика вправо" |   popRight: "Из столбика вправо" | ||||||
|   profile: "Профиль" |   profile: "Расстановка" | ||||||
|  |   newProfile: "Новая расстановка" | ||||||
|  |   deleteProfile: "Удаление расстановки" | ||||||
|  |   introduction: "Создайте идеальный интерфейс расставляя колонки как угодно" | ||||||
|  |   introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана." | ||||||
|  |   widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки." | ||||||
|   _columns: |   _columns: | ||||||
|     main: "Основная" |     main: "Основная" | ||||||
|     widgets: "Виджеты" |     widgets: "Виджеты" | ||||||
|   | |||||||
| @@ -944,48 +944,236 @@ _achievements: | |||||||
|   _types: |   _types: | ||||||
|     _notes1: |     _notes1: | ||||||
|       title: "เพียงแค่ตั้งค่า msky ของฉัน" |       title: "เพียงแค่ตั้งค่า msky ของฉัน" | ||||||
|  |       description: "โพสต์โน้ตครั้งแรกของคุณ" | ||||||
|  |       flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!" | ||||||
|  |     _notes10: | ||||||
|  |       title: "โน้ตบางอย่าง" | ||||||
|  |       description: "โพสต์ 10 โน้ต" | ||||||
|  |     _notes100: | ||||||
|  |       title: "โน้ตจำนวนมาก" | ||||||
|  |       description: "โพสต์ 100 โน้ต" | ||||||
|  |     _notes500: | ||||||
|  |       title: "ครอบคลุมในโน้ต" | ||||||
|  |       description: "โพสต์ 500 โน้ต" | ||||||
|  |     _notes1000: | ||||||
|  |       title: "ภูเขาแห่งโน้ต" | ||||||
|  |       description: "โพสต์ 1,000 โน้ต" | ||||||
|  |     _notes5000: | ||||||
|  |       title: "โน้ตล้น" | ||||||
|  |       description: "โพสต์ 5,000 โน้ต" | ||||||
|  |     _notes10000: | ||||||
|  |       title: "ซุปเปอร์โน้ต" | ||||||
|  |       description: "โพสต์ 10,000 โน้ต" | ||||||
|  |     _notes20000: | ||||||
|  |       title: "ต้องการ... เพิ่มเติม... โน้ต..." | ||||||
|  |       description: "โพสต์ 20,000 โน้ต" | ||||||
|  |     _notes30000: | ||||||
|  |       title: "โน้ต โน้ต โน้ต!" | ||||||
|  |       description: "โพสต์ 30,000 โน้ต" | ||||||
|  |     _notes40000: | ||||||
|  |       title: "โน้ตโรงงาน" | ||||||
|  |       description: "โพสต์ 40,000 โน้ต" | ||||||
|  |     _notes50000: | ||||||
|  |       title: "ดาวเคราะห์แห่งโน้ต" | ||||||
|  |       description: "โพสต์ 50,000 โน้ต" | ||||||
|  |     _notes60000: | ||||||
|  |       title: "โน้ตควอซาร์" | ||||||
|  |       description: "โพสต์ 60,000 โน้ต" | ||||||
|  |     _notes70000: | ||||||
|  |       title: "โน้ตหลุมดำ" | ||||||
|  |       description: "โพสต์ 70,000 โน้ต" | ||||||
|  |     _notes80000: | ||||||
|  |       title: "โน้ต กาแล็กซี่" | ||||||
|  |       description: "โพสต์ 80,000 โน้ต" | ||||||
|  |     _notes90000: | ||||||
|  |       title: "โน้ต จักรวาล" | ||||||
|  |       description: "โพสต์ 90,000 โน้ต" | ||||||
|  |     _notes100000: | ||||||
|  |       title: "ALL YOUR NOTE ARE BELONG TO US" | ||||||
|  |       description: "โพสต์ 100,000 โน้ต" | ||||||
|  |       flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ" | ||||||
|  |     _login3: | ||||||
|  |       title: "มือใหม่ I" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน" | ||||||
|  |       flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์" | ||||||
|  |     _login7: | ||||||
|  |       title: "มือใหม่ II" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน" | ||||||
|  |       flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?" | ||||||
|  |     _login15: | ||||||
|  |       title: "มือใหม่ III" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน" | ||||||
|  |     _login30: | ||||||
|  |       title: "มิสคิสท์ I" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน" | ||||||
|  |     _login60: | ||||||
|  |       title: "มิสคิสท์ II" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน" | ||||||
|  |     _login100: | ||||||
|  |       title: "มิสคิสท์ III" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน" | ||||||
|  |       flavor: "ความรุนแรง Misskist" | ||||||
|  |     _login200: | ||||||
|  |       title: "ลูกค้าประจำ I" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน" | ||||||
|  |     _login300: | ||||||
|  |       title: "ลูกค้าประจำ II" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 300 วัน" | ||||||
|  |     _login400: | ||||||
|  |       title: "ลูกค้าประจำ III" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 400 วัน" | ||||||
|  |     _login500: | ||||||
|  |       title: "ผู้เชี่ยวชาญ I" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน" | ||||||
|  |       flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต" | ||||||
|  |     _login600: | ||||||
|  |       title: "ผู้เชี่ยวชาญ II" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน" | ||||||
|  |     _login700: | ||||||
|  |       title: "ผู้เชี่ยวชาญ III" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 700 วัน" | ||||||
|  |     _login800: | ||||||
|  |       title: "ปรมาจารย์ด้านโน้ต I" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 800 วัน" | ||||||
|  |     _login900: | ||||||
|  |       title: "ปรมาจารย์ด้านโน้ต II" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 900 วัน" | ||||||
|  |     _login1000: | ||||||
|  |       title: "ปรมาจารย์ด้านโน้ต III" | ||||||
|  |       description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน" | ||||||
|  |       flavor: "ขอบคุณที่ใช้ Misskey นะ !" | ||||||
|  |     _noteClipped1: | ||||||
|  |       title: "จะต้อง... คลิป..." | ||||||
|  |       description: "คลิปโน้ตตัวแรกของคุณ" | ||||||
|  |     _noteFavorited1: | ||||||
|  |       title: "สตาร์เกเซอร์" | ||||||
|  |       description: "ชื่นชอบโน้ตแรกของคุณ" | ||||||
|  |     _myNoteFavorited1: | ||||||
|  |       title: "แสวงหาดวงดาว" | ||||||
|  |       description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ" | ||||||
|  |     _profileFilled: | ||||||
|  |       title: "เตรียมไว้อย่างดี" | ||||||
|  |       description: "ตั้งค่าโปรไฟล์ของคุณ" | ||||||
|  |     _markedAsCat: | ||||||
|  |       title: "ฉันเป็นแมว" | ||||||
|  |       description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว" | ||||||
|  |       flavor: "ฉันจะให้ชื่อคุณภายหลังนะ" | ||||||
|  |     _following1: | ||||||
|  |       title: "กำลังติดตามผู้ใช้คนแรกของคุณ" | ||||||
|  |       description: "ติดตามผู้ใช้" | ||||||
|  |     _following10: | ||||||
|  |       title: "ทำต่อไป... ทำต่อไป..." | ||||||
|  |       description: "ติดตาม 10 บัญชีผู้ใช้" | ||||||
|  |     _following50: | ||||||
|  |       title: "มีเพื่อนมากมาย" | ||||||
|  |       description: "ติดตาม 50 บัญชี" | ||||||
|  |     _following100: | ||||||
|  |       title: "เพื่อน 100 คน" | ||||||
|  |       description: "ติดตาม 100 บัญชี" | ||||||
|  |     _following300: | ||||||
|  |       title: "เพื่อนโอเวอร์โหลด" | ||||||
|  |       description: "ติดตาม 300 บัญชี" | ||||||
|  |     _followers1: | ||||||
|  |       title: "ผู้ติดตามคนแรก" | ||||||
|  |       description: "ได้รับ 1 ผู้ติดตาม" | ||||||
|  |     _followers10: | ||||||
|  |       title: "ติดตามฉัน!" | ||||||
|  |       description: "ได้รับ 10 คนผู้ติดตาม" | ||||||
|  |     _followers50: | ||||||
|  |       title: "มากันเป็นฝูง" | ||||||
|  |       description: "ได้รับ 50 ผู้ติดตาม" | ||||||
|     _followers100: |     _followers100: | ||||||
|       title: "บุคคลที่เป็นที่นิยม" |       title: "บุคคลที่เป็นที่นิยม" | ||||||
|  |       description: "ได้รับ 100 ผู้ติดตาม" | ||||||
|  |     _followers300: | ||||||
|  |       title: "กรุณาสร้างบรรทัดเดียวนะคะ" | ||||||
|  |       description: "ได้รับ 300 คนผู้ติดตาม" | ||||||
|     _followers500: |     _followers500: | ||||||
|       title: "เสาสัญญาณ" |       title: "เสาสัญญาณ" | ||||||
|  |       description: "ได้รับ 500 คนผู้ติดตาม" | ||||||
|     _followers1000: |     _followers1000: | ||||||
|       title: "ผู้ทรงอิทธิพล" |       title: "ผู้ทรงอิทธิพล" | ||||||
|  |       description: "ได้รับ 1,000 ผู้ติดตาม" | ||||||
|  |     _collectAchievements30: | ||||||
|  |       title: "นักสะสมความสำเร็จ" | ||||||
|  |       description: "ได้รับความสำเร็จ 30 ครั้ง" | ||||||
|  |     _viewAchievements3min: | ||||||
|  |       title: "ชอบบรรลุผลสําเร็จ" | ||||||
|  |       description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที" | ||||||
|     _iLoveMisskey: |     _iLoveMisskey: | ||||||
|       title: "ฉันรัก Misskey" |       title: "ฉันรัก Misskey" | ||||||
|       description: "โพสต์ \"I ❤ #Misskey\"" |       description: "โพสต์ \"I ❤ #Misskey\"" | ||||||
|  |       flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!" | ||||||
|     _foundTreasure: |     _foundTreasure: | ||||||
|       title: "ล่าสมบัติ" |       title: "ล่าสมบัติ" | ||||||
|       description: "คุณพบสมบัติที่ซ่อนอยู่" |       description: "คุณพบสมบัติที่ซ่อนอยู่" | ||||||
|     _client30min: |     _client30min: | ||||||
|       title: "พักผ่อนสักหน่อย" |       title: "พักผ่อนสักหน่อย" | ||||||
|  |       description: "ใช้เวลา 30 นาทีบน Misskey" | ||||||
|     _noteDeletedWithin1min: |     _noteDeletedWithin1min: | ||||||
|       title: "ไม่เป็นไร" |       title: "ไม่เป็นไร" | ||||||
|  |       description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์" | ||||||
|     _postedAtLateNight: |     _postedAtLateNight: | ||||||
|       title: "กลางคืน" |       title: "กลางคืน" | ||||||
|  |       description: "โพสต์โน้ตตอนดึกๆ" | ||||||
|  |       flavor: "ได้เวลาเข้านอนแล้วนะ" | ||||||
|  |     _postedAt0min0sec: | ||||||
|  |       title: "นาฬิกาพูดได้" | ||||||
|  |       description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น." | ||||||
|  |       flavor: "คลิก คลิก คลิก แกล๊งๆ" | ||||||
|  |     _selfQuote: | ||||||
|  |       title: "อ้างอิงตนเอง" | ||||||
|  |       description: "อ้างโน้ตย่อของคุณเอง" | ||||||
|  |     _htl20npm: | ||||||
|  |       title: "ไทม์ไลน์ไหล" | ||||||
|  |       description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)" | ||||||
|     _viewInstanceChart: |     _viewInstanceChart: | ||||||
|       title: "วิเคราะห์" |       title: "วิเคราะห์" | ||||||
|       description: "ดูแผนภูมิอินสแตนซ์ของคุณ" |       description: "ดูแผนภูมิอินสแตนซ์ของคุณ" | ||||||
|  |     _outputHelloWorldOnScratchpad: | ||||||
|  |       title: "หวัดดีชาวโลก!" | ||||||
|  |       description: "เอาพุต \"hello world\" ใน Scratchpad" | ||||||
|  |     _open3windows: | ||||||
|  |       title: "มัลติวินโดว์" | ||||||
|  |       description: "มีการเปิดหน้าต่างอย่างน้อย 3 หน้าต่างพร้อมกัน" | ||||||
|     _driveFolderCircularReference: |     _driveFolderCircularReference: | ||||||
|       title: "อ้างอิงวงจร" |       title: "อ้างอิงวงจร" | ||||||
|  |       description: "พยายามสร้างโฟลเดอร์ที่ซ้อนกันแบบวนซ้ำในไดรฟ์" | ||||||
|  |     _reactWithoutRead: | ||||||
|  |       title: "คุณอ่านมันจริงๆหรือเปล่า?" | ||||||
|  |       description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์" | ||||||
|     _clickedClickHere: |     _clickedClickHere: | ||||||
|       title: "คลิ๊กที่นี่" |       title: "คลิ๊กที่นี่" | ||||||
|       description: "คุณได้คลิกที่นี่" |       description: "คุณได้คลิกที่นี่" | ||||||
|  |     _justPlainLucky: | ||||||
|  |       title: "แค่ลัคกี้ธรรมดา" | ||||||
|  |       description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที" | ||||||
|  |     _setNameToSyuilo: | ||||||
|  |       title: "พระเจ้าคอมเพล็กซ์" | ||||||
|  |       description: "ตั้งชื่อของคุณเป็น \"syuilo\"" | ||||||
|     _passedSinceAccountCreated1: |     _passedSinceAccountCreated1: | ||||||
|       title: "ครบรอบหนึ่งปี" |       title: "ครบรอบหนึ่งปี" | ||||||
|  |       description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" | ||||||
|     _passedSinceAccountCreated2: |     _passedSinceAccountCreated2: | ||||||
|       title: "ครบรอบสองปี" |       title: "ครบรอบสองปี" | ||||||
|  |       description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" | ||||||
|     _passedSinceAccountCreated3: |     _passedSinceAccountCreated3: | ||||||
|       title: "ครบรอบสามปี" |       title: "ครบรอบสามปี" | ||||||
|  |       description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" | ||||||
|     _loggedInOnBirthday: |     _loggedInOnBirthday: | ||||||
|       title: "สุขสันต์วันเกิด" |       title: "สุขสันต์วันเกิด" | ||||||
|       description: "เข้าสู่ระบบในวันเกิดของคุณ" |       description: "เข้าสู่ระบบในวันเกิดของคุณ" | ||||||
|     _loggedInOnNewYearsDay: |     _loggedInOnNewYearsDay: | ||||||
|       title: "สวัสดีปีใหม่!" |       title: "สวัสดีปีใหม่!" | ||||||
|       description: "เข้าสู่ระบบในวันแรกของปี" |       description: "เข้าสู่ระบบในวันแรกของปี" | ||||||
|  |       flavor: "อีกปีที่ยอดเยี่ยมในโอกาสนี้เลย" | ||||||
|     _cookieClicked: |     _cookieClicked: | ||||||
|  |       title: "เกมที่คุณคลิกที่คุกกี้" | ||||||
|       description: "คลิกคุกกี้" |       description: "คลิกคุกกี้" | ||||||
|  |       flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?" | ||||||
|     _brainDiver: |     _brainDiver: | ||||||
|       title: "Brain Diver" |       title: "Brain Diver" | ||||||
|  |       description: "โพสต์ลิงก์ไปยัง Brain Diver" | ||||||
|       flavor: "Misskey-Misskey La-Tu-Ma" |       flavor: "Misskey-Misskey La-Tu-Ma" | ||||||
| _role: | _role: | ||||||
|   new: "บทบาทใหม่" |   new: "บทบาทใหม่" | ||||||
| @@ -994,7 +1182,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: "มีเงื่อนไข" | ||||||
| @@ -1007,6 +1195,9 @@ _role: | |||||||
|   baseRole: "บทบาทพื้นฐาน" |   baseRole: "บทบาทพื้นฐาน" | ||||||
|   useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" |   useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น" | ||||||
|   chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" |   chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" | ||||||
|  |   iconUrl: "ไอคอน URL" | ||||||
|  |   asBadge: "แสดงเป็นตรา" | ||||||
|  |   descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน" | ||||||
|   canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" |   canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก" | ||||||
|   descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" |   descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ" | ||||||
|   priority: "ลำดับความสำคัญ" |   priority: "ลำดับความสำคัญ" | ||||||
|   | |||||||
| @@ -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: "Скрипт" | ||||||
| @@ -904,7 +904,7 @@ _achievements: | |||||||
|   earnedAt: "Відкрито" |   earnedAt: "Відкрито" | ||||||
|   _types: |   _types: | ||||||
|     _notes1: |     _notes1: | ||||||
|       title: "налаштовую свій msky" |       title: "Привіт, Misskey!" | ||||||
|       description: "Перша нотатка" |       description: "Перша нотатка" | ||||||
|       flavor: "Приємного часу з Misskey!" |       flavor: "Приємного часу з Misskey!" | ||||||
|     _notes10: |     _notes10: | ||||||
| @@ -1042,6 +1042,7 @@ _achievements: | |||||||
|       title: "Популярна особа" |       title: "Популярна особа" | ||||||
|       description: "Кількість підписників досягла 100" |       description: "Кількість підписників досягла 100" | ||||||
|     _followers300: |     _followers300: | ||||||
|  |       title: "Ставайте в чергу" | ||||||
|       description: "Кількість підписників досягла 300" |       description: "Кількість підписників досягла 300" | ||||||
|     _followers500: |     _followers500: | ||||||
|       title: "Радіовежа" |       title: "Радіовежа" | ||||||
| @@ -1049,19 +1050,70 @@ _achievements: | |||||||
|     _followers1000: |     _followers1000: | ||||||
|       title: "Інфлюенсер" |       title: "Інфлюенсер" | ||||||
|       description: "Кількість підписників досягла 1000" |       description: "Кількість підписників досягла 1000" | ||||||
|  |     _collectAchievements30: | ||||||
|  |       title: "Збирач досягнень" | ||||||
|  |       description: "Отримано 30 досягнень" | ||||||
|  |     _viewAchievements3min: | ||||||
|  |       title: "Шанувальник досягнень" | ||||||
|  |       description: "Переглядати список досягнень принаймні 3 хвилини" | ||||||
|  |     _iLoveMisskey: | ||||||
|  |       title: "I Love Misskey" | ||||||
|  |       description: "Відправлено \"I ❤ #Misskey\"" | ||||||
|  |       flavor: "Дякуємо вам, що користуєтесь Misskey!  – команда розробників" | ||||||
|  |     _foundTreasure: | ||||||
|  |       title: "Пошуки скарбів" | ||||||
|  |       description: "Ви знайшли прихований скарб" | ||||||
|  |     _client30min: | ||||||
|  |       title: "Коротка перерва" | ||||||
|  |       description: "З моменту запуску клієнта минуло 30 хвилин" | ||||||
|  |     _noteDeletedWithin1min: | ||||||
|  |       title: "Не зважай" | ||||||
|  |       description: "Допис видалено протягом 1 хвилини після публікації" | ||||||
|  |     _postedAtLateNight: | ||||||
|  |       title: "Нічне життя" | ||||||
|  |       description: "Відправити нотатку посеред ночі" | ||||||
|  |       flavor: "Час лягати спати" | ||||||
|  |     _postedAt0min0sec: | ||||||
|  |       title: "Сигнал часу" | ||||||
|  |       description: "Відправити нотатку о 00:00" | ||||||
|  |     _selfQuote: | ||||||
|  |       title: "Самопосилання" | ||||||
|  |       description: "Процитувати власну нотатку" | ||||||
|  |     _htl20npm: | ||||||
|  |       title: "Плинна стрічка" | ||||||
|  |       description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)" | ||||||
|  |     _viewInstanceChart: | ||||||
|  |       title: "Аналітик" | ||||||
|  |     _outputHelloWorldOnScratchpad: | ||||||
|  |       title: "Hello, world!" | ||||||
|  |       description: "Вивести \"hello world\" у Скретчпаді" | ||||||
|  |     _clickedClickHere: | ||||||
|  |       title: "Натисніть тут" | ||||||
|  |       description: "Натиснуто тут" | ||||||
|  |     _justPlainLucky: | ||||||
|  |       title: "Просто вдача" | ||||||
|  |       description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд" | ||||||
|  |     _setNameToSyuilo: | ||||||
|  |       title: "Комплекс бога" | ||||||
|  |       description: "Встановлено ім'я \"syuilo\"" | ||||||
|     _passedSinceAccountCreated1: |     _passedSinceAccountCreated1: | ||||||
|       title: "Перша річниця" |       title: "Перша річниця" | ||||||
|  |       description: "Минув рік з моменту створення акаунта" | ||||||
|     _passedSinceAccountCreated2: |     _passedSinceAccountCreated2: | ||||||
|       title: "Друга річниця" |       title: "Друга річниця" | ||||||
|  |       description: "Минуло 2 роки з моменту створення акаунта" | ||||||
|     _passedSinceAccountCreated3: |     _passedSinceAccountCreated3: | ||||||
|       title: "Третя річниця" |       title: "Третя річниця" | ||||||
|       description: "Минуло 3 роки з моменту створення акаунта" |       description: "Минуло 3 роки з моменту створення акаунта" | ||||||
|     _loggedInOnBirthday: |     _loggedInOnBirthday: | ||||||
|       title: "З Днем народження!" |       title: "З Днем народження!" | ||||||
|  |       description: "Увійти у свій день народження" | ||||||
|     _loggedInOnNewYearsDay: |     _loggedInOnNewYearsDay: | ||||||
|  |       title: "З Новим роком!" | ||||||
|       description: "Увійшли в перший день року" |       description: "Увійшли в перший день року" | ||||||
|     _brainDiver: |     _brainDiver: | ||||||
|       title: "Brain Diver" |       title: "Brain Diver" | ||||||
|  |       description: "Відправити посилання на \"Brain Diver\"" | ||||||
|       flavor: "Misskey-Misskey La-Tu-Ma" |       flavor: "Misskey-Misskey La-Tu-Ma" | ||||||
| _role: | _role: | ||||||
|   priority: "Пріоритет" |   priority: "Пріоритет" | ||||||
| @@ -1330,12 +1382,12 @@ _tutorial: | |||||||
|   step1_1: "Ласкаво просимо!" |   step1_1: "Ласкаво просимо!" | ||||||
|   step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані." |   step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані." | ||||||
|   step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших." |   step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших." | ||||||
|   step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис." |   step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль." | ||||||
|   step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас." |   step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас." | ||||||
|   step3_1: "Ви успішно налаштували свій обліковий запис?" |   step3_1: "Ви успішно налаштували свій обліковий запис?" | ||||||
|   step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані." |   step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані." | ||||||
|   step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми." |   step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми." | ||||||
|   step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!" |   step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\"" | ||||||
|   step4_1: "Ви розмістили свій перший запис?" |   step4_1: "Ви розмістили свій перший запис?" | ||||||
|   step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій." |   step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій." | ||||||
|   step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів." |   step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів." | ||||||
|   | |||||||
| @@ -995,52 +995,170 @@ _achievements: | |||||||
|     _login3: |     _login3: | ||||||
|       title: "初学者 I" |       title: "初学者 I" | ||||||
|       description: "连续登录3天" |       description: "连续登录3天" | ||||||
|  |       flavor: "今天开始我就是Misskist!" | ||||||
|     _login7: |     _login7: | ||||||
|  |       title: "初学者 II" | ||||||
|       description: "连续登录7天" |       description: "连续登录7天" | ||||||
|  |       flavor: "您开始习惯了吗?" | ||||||
|     _login15: |     _login15: | ||||||
|  |       title: "初学者 III" | ||||||
|       description: "连续登录15天" |       description: "连续登录15天" | ||||||
|     _login30: |     _login30: | ||||||
|  |       title: "Misskist Ⅰ" | ||||||
|       description: "连续登录30天" |       description: "连续登录30天" | ||||||
|     _login60: |     _login60: | ||||||
|  |       title: "Misskist Ⅱ" | ||||||
|       description: "连续登录60天" |       description: "连续登录60天" | ||||||
|  |     _login100: | ||||||
|  |       title: "Misskist Ⅲ" | ||||||
|  |       description: "总登入100天" | ||||||
|  |       flavor: "那个用户,是Misskist喔" | ||||||
|  |     _login200: | ||||||
|  |       title: "定期联系Ⅰ" | ||||||
|  |       description: "总登录天数200天" | ||||||
|  |     _login300: | ||||||
|  |       title: "定期联系Ⅱ" | ||||||
|  |       description: "总登录天数300天" | ||||||
|  |     _login400: | ||||||
|  |       title: "定期联系Ⅲ" | ||||||
|  |       description: "总登录天数400天" | ||||||
|  |     _login500: | ||||||
|  |       title: "老熟人Ⅰ" | ||||||
|  |       description: "总登录天数500天" | ||||||
|  |       flavor: "诸君,我喜欢贴文" | ||||||
|  |     _login600: | ||||||
|  |       title: "老熟人Ⅱ" | ||||||
|  |       description: "总登录天数600天" | ||||||
|  |     _login700: | ||||||
|  |       title: "老熟人Ⅲ" | ||||||
|  |       description: "总登录天数700天" | ||||||
|  |     _login800: | ||||||
|  |       title: "帖子大师Ⅰ" | ||||||
|  |       description: "总登录天数800天" | ||||||
|  |     _login900: | ||||||
|  |       title: "帖子大师Ⅱ" | ||||||
|  |       description: "总登录天数900天" | ||||||
|     _login1000: |     _login1000: | ||||||
|  |       title: "帖子大师Ⅲ" | ||||||
|  |       description: "总登录天数1000天" | ||||||
|       flavor: "感谢您使用Misskey!" |       flavor: "感谢您使用Misskey!" | ||||||
|  |     _noteClipped1: | ||||||
|  |       title: "忍不住要收藏到便签" | ||||||
|  |       description: "第一次将贴文贴进便签" | ||||||
|     _noteFavorited1: |     _noteFavorited1: | ||||||
|       title: "观星者" |       title: "观星者" | ||||||
|  |       description: "第一次将帖子加入收藏" | ||||||
|  |     _myNoteFavorited1: | ||||||
|  |       title: "想要星星" | ||||||
|  |       description: "自己的帖子被其他人加入收藏了" | ||||||
|     _profileFilled: |     _profileFilled: | ||||||
|       title: "整装待发" |       title: "整装待发" | ||||||
|       description: "设置了个人资料" |       description: "设置了个人资料" | ||||||
|     _markedAsCat: |     _markedAsCat: | ||||||
|       title: "我是猫" |       title: "我是猫" | ||||||
|       description: "将账户设定为一只猫" |       description: "将账户设定为一只猫" | ||||||
|  |       flavor: "还没有名字" | ||||||
|  |     _following1: | ||||||
|  |       title: "首次关注" | ||||||
|  |       description: "第一次关注别人" | ||||||
|     _following10: |     _following10: | ||||||
|       title: "关注,跟随" |       title: "关注,跟随" | ||||||
|  |       description: "关注超过10人" | ||||||
|     _following50: |     _following50: | ||||||
|       title: "我的朋友很多" |       title: "我的朋友很多" | ||||||
|  |       description: "关注超过50人" | ||||||
|  |     _following100: | ||||||
|  |       title: "我的朋友很多" | ||||||
|  |       description: "关注超过100人" | ||||||
|     _following300: |     _following300: | ||||||
|  |       title: "朋友成群" | ||||||
|       description: "关注数超过300" |       description: "关注数超过300" | ||||||
|  |     _followers1: | ||||||
|  |       title: "最初的关注者" | ||||||
|  |       description: "第一次被关注" | ||||||
|  |     _followers10: | ||||||
|  |       title: "关注我吧!" | ||||||
|  |       description: "拥有超过10名关注者" | ||||||
|  |     _followers50: | ||||||
|  |       title: "三五成群" | ||||||
|  |       description: "拥有超过50名关注者" | ||||||
|     _followers100: |     _followers100: | ||||||
|       title: "胜友如云" |       title: "胜友如云" | ||||||
|  |       description: "拥有超过100名关注者" | ||||||
|  |     _followers300: | ||||||
|  |       title: "排列成行" | ||||||
|  |       description: "拥有超过300名关注者" | ||||||
|  |     _followers500: | ||||||
|  |       title: "信号塔" | ||||||
|  |       description: "拥有超过500名关注者" | ||||||
|  |     _followers1000: | ||||||
|  |       title: "大影响家" | ||||||
|  |       description: "拥有超过1000名关注者" | ||||||
|     _collectAchievements30: |     _collectAchievements30: | ||||||
|  |       title: "成就收藏家" | ||||||
|       description: "获得超过30个成就" |       description: "获得超过30个成就" | ||||||
|     _viewAchievements3min: |     _viewAchievements3min: | ||||||
|  |       title: "成就爱好者" | ||||||
|       description: "盯着成就看三分钟" |       description: "盯着成就看三分钟" | ||||||
|     _iLoveMisskey: |     _iLoveMisskey: | ||||||
|       title: "I Love Misskey" |       title: "I Love Misskey" | ||||||
|       description: "发布\"I ❤ #Misskey\"帖子" |       description: "发布\"I ❤ #Misskey\"帖子" | ||||||
|       flavor: "感谢您使用 Misskey ! by 开发团队" |       flavor: "感谢您使用 Misskey ! by 开发团队" | ||||||
|  |     _foundTreasure: | ||||||
|  |       title: "寻宝" | ||||||
|  |       description: "发现了隐藏的宝藏" | ||||||
|  |     _client30min: | ||||||
|  |       title: "休息一下!" | ||||||
|  |       description: "启动客户端超过30分钟" | ||||||
|     _noteDeletedWithin1min: |     _noteDeletedWithin1min: | ||||||
|  |       title: "无话可说" | ||||||
|       description: "发帖后一分钟内就将其删除" |       description: "发帖后一分钟内就将其删除" | ||||||
|     _postedAtLateNight: |     _postedAtLateNight: | ||||||
|       title: "夜行者" |       title: "夜猫子" | ||||||
|       description: "深夜发布帖子" |       description: "深夜发布帖子" | ||||||
|  |       flavor: "差不多该去睡了喔。" | ||||||
|  |     _postedAt0min0sec: | ||||||
|  |       title: "报时" | ||||||
|  |       description: "在0点发布一篇帖子" | ||||||
|  |       flavor: "嘣 嘣 嘣 Biu——!" | ||||||
|  |     _selfQuote: | ||||||
|  |       title: "自我提及" | ||||||
|  |       description: "引用了自己的帖子" | ||||||
|  |     _htl20npm: | ||||||
|  |       title: "流动的时间线" | ||||||
|  |       description: "在首页时间线的流速超过20npm" | ||||||
|  |     _viewInstanceChart: | ||||||
|  |       title: "分析师" | ||||||
|  |       description: "查看了实例信息中的图表" | ||||||
|     _outputHelloWorldOnScratchpad: |     _outputHelloWorldOnScratchpad: | ||||||
|       title: "Hello, world!" |       title: "Hello, world!" | ||||||
|  |       description: "在AiScript控制台中输出 hello world" | ||||||
|  |     _open3windows: | ||||||
|  |       title: "多窗口" | ||||||
|  |       description: "打开了三个或更多的窗口" | ||||||
|  |     _driveFolderCircularReference: | ||||||
|  |       title: "循环引用" | ||||||
|  |       description: "试图对网盘中的文件夹进行循环嵌套" | ||||||
|  |     _reactWithoutRead: | ||||||
|  |       title: "有好好读过吗?" | ||||||
|  |       description: "在含有100字以上的帖子被发出三秒内做出回应" | ||||||
|  |     _clickedClickHere: | ||||||
|  |       title: "点这里" | ||||||
|  |       description: "点了这里" | ||||||
|  |     _justPlainLucky: | ||||||
|  |       title: "超高校级的幸运" | ||||||
|  |       description: "每10秒有0.01的概率自动获得" | ||||||
|  |     _setNameToSyuilo: | ||||||
|  |       title: "像神一样呐" | ||||||
|  |       description: "将名称设定为syuilo" | ||||||
|     _passedSinceAccountCreated1: |     _passedSinceAccountCreated1: | ||||||
|  |       title: "一周年" | ||||||
|       description: "账户创建时间超过1年" |       description: "账户创建时间超过1年" | ||||||
|     _passedSinceAccountCreated2: |     _passedSinceAccountCreated2: | ||||||
|  |       title: "二周年" | ||||||
|       description: "账户创建时间超过2年" |       description: "账户创建时间超过2年" | ||||||
|     _passedSinceAccountCreated3: |     _passedSinceAccountCreated3: | ||||||
|  |       title: "三周年" | ||||||
|       description: "账户创建时间超过3年" |       description: "账户创建时间超过3年" | ||||||
|     _loggedInOnBirthday: |     _loggedInOnBirthday: | ||||||
|       title: "生日快乐" |       title: "生日快乐" | ||||||
| @@ -1048,6 +1166,15 @@ _achievements: | |||||||
|     _loggedInOnNewYearsDay: |     _loggedInOnNewYearsDay: | ||||||
|       title: "恭贺新禧" |       title: "恭贺新禧" | ||||||
|       description: "在元旦登入" |       description: "在元旦登入" | ||||||
|  |       flavor: "今年也请对本实例多多指教!" | ||||||
|  |     _cookieClicked: | ||||||
|  |       title: "点击饼干小游戏" | ||||||
|  |       description: "点击了可疑的饼干" | ||||||
|  |       flavor: "是不是软件有问题?" | ||||||
|  |     _brainDiver: | ||||||
|  |       title: "Brain Diver" | ||||||
|  |       description: "发布了包含Brain Diver链接的帖子" | ||||||
|  |       flavor: "Misskey-Misskey La-Tu-Ma" | ||||||
| _role: | _role: | ||||||
|   new: "创建角色" |   new: "创建角色" | ||||||
|   edit: "编辑角色" |   edit: "编辑角色" | ||||||
| @@ -1068,6 +1195,9 @@ _role: | |||||||
|   baseRole: "基本角色" |   baseRole: "基本角色" | ||||||
|   useBaseValue: "使用基本角色的值" |   useBaseValue: "使用基本角色的值" | ||||||
|   chooseRoleToAssign: "选择要分配的角色" |   chooseRoleToAssign: "选择要分配的角色" | ||||||
|  |   iconUrl: "图标URL" | ||||||
|  |   asBadge: "作为徽章显示" | ||||||
|  |   descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。" | ||||||
|   canEditMembersByModerator: "允许监察者编辑成员" |   canEditMembersByModerator: "允许监察者编辑成员" | ||||||
|   descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" |   descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" | ||||||
|   priority: "优先级" |   priority: "优先级" | ||||||
| @@ -1566,7 +1696,7 @@ _profile: | |||||||
|   name: "昵称" |   name: "昵称" | ||||||
|   username: "用户名" |   username: "用户名" | ||||||
|   description: "个人简介" |   description: "个人简介" | ||||||
|   youCanIncludeHashtags: "您可以包含一个哈希标签。" |   youCanIncludeHashtags: "你可以在个人简介中包含一个#标签。" | ||||||
|   metadata: "附加信息" |   metadata: "附加信息" | ||||||
|   metadataEdit: "附加信息编辑" |   metadataEdit: "附加信息编辑" | ||||||
|   metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" |   metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。" | ||||||
|   | |||||||
| @@ -240,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?" | |||||||
| deleteAreYouSure: "確定要刪掉「{x}」嗎?" | deleteAreYouSure: "確定要刪掉「{x}」嗎?" | ||||||
| resetAreYouSure: "確定要重設嗎?" | resetAreYouSure: "確定要重設嗎?" | ||||||
| saved: "已儲存" | saved: "已儲存" | ||||||
| messaging: "傳送訊息" | messaging: "聊天" | ||||||
| upload: "上傳" | upload: "上傳" | ||||||
| keepOriginalUploading: "保留原圖" | keepOriginalUploading: "保留原圖" | ||||||
| keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" | keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" | ||||||
| @@ -326,15 +326,15 @@ connectService: "己連結" | |||||||
| disconnectService: "己斷開 " | disconnectService: "己斷開 " | ||||||
| enableLocalTimeline: "開啟本地時間軸" | enableLocalTimeline: "開啟本地時間軸" | ||||||
| enableGlobalTimeline: "啟用全域時間軸" | enableGlobalTimeline: "啟用全域時間軸" | ||||||
| disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。" | disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審查員仍可以繼續使用。" | ||||||
| registration: "註冊" | registration: "註冊" | ||||||
| enableRegistration: "開啟新使用者註冊" | enableRegistration: "開啟新使用者註冊" | ||||||
| invite: "邀請" | invite: "邀請" | ||||||
| driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" | driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" | ||||||
| driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量" | driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小" | ||||||
| inMb: "以Mbps為單位" | inMb: "以Mbps為單位" | ||||||
| iconUrl: "圖像URL" | iconUrl: "圖標URL" | ||||||
| bannerUrl: "橫幅圖像URL" | bannerUrl: "橫幅圖片URL" | ||||||
| backgroundImageUrl: "背景圖片的來源網址 " | backgroundImageUrl: "背景圖片的來源網址 " | ||||||
| basicInfo: "基本資訊" | basicInfo: "基本資訊" | ||||||
| pinnedUsers: "置頂用戶" | pinnedUsers: "置頂用戶" | ||||||
| @@ -373,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶" | |||||||
| notesAndReplies: "貼文與回覆" | notesAndReplies: "貼文與回覆" | ||||||
| withFiles: "附件" | withFiles: "附件" | ||||||
| silence: "禁言" | silence: "禁言" | ||||||
| silenceConfirm: "確定要禁言此用戶嗎?" | silenceConfirm: "確定要靜音此使用者嗎?" | ||||||
| unsilence: "解除禁言" | unsilence: "解除靜音" | ||||||
| unsilenceConfirm: "確定要解除禁言嗎?" | unsilenceConfirm: "確定要解除禁言嗎?" | ||||||
| popularUsers: "熱門使用者" | popularUsers: "熱門使用者" | ||||||
| recentlyUpdatedUsers: "最近發文的使用者" | recentlyUpdatedUsers: "最近發文的使用者" | ||||||
| @@ -383,14 +383,14 @@ recentlyDiscoveredUsers: "最近發現的使用者" | |||||||
| exploreUsersCount: "有{count}個使用者" | exploreUsersCount: "有{count}個使用者" | ||||||
| exploreFediverse: "探索聯邦世界" | exploreFediverse: "探索聯邦世界" | ||||||
| popularTags: "熱門標籤" | popularTags: "熱門標籤" | ||||||
| userList: "清單" | userList: "使用者清單" | ||||||
| about: "資訊" | about: "關於" | ||||||
| aboutMisskey: "關於 Misskey" | aboutMisskey: "關於 Misskey" | ||||||
| administrator: "管理員" | administrator: "管理員" | ||||||
| token: "權杖" | token: "權杖" | ||||||
| twoStepAuthentication: "兩階段驗證" | twoStepAuthentication: "兩階段驗證" | ||||||
| moderator: "監察員" | moderator: "審查員" | ||||||
| moderation: "監察" | moderation: "審查" | ||||||
| nUsersMentioned: "提到了{n}" | nUsersMentioned: "提到了{n}" | ||||||
| securityKey: "安全金鑰" | securityKey: "安全金鑰" | ||||||
| securityKeyName: "金鑰名稱" | securityKeyName: "金鑰名稱" | ||||||
| @@ -421,7 +421,7 @@ invites: "邀請" | |||||||
| groupName: "群組名稱" | groupName: "群組名稱" | ||||||
| members: "成員" | members: "成員" | ||||||
| transfer: "轉讓" | transfer: "轉讓" | ||||||
| messagingWithUser: "傳送訊息給其他使用者" | messagingWithUser: "與其他使用者聊天" | ||||||
| messagingWithGroup: "發送訊息至群組" | messagingWithGroup: "發送訊息至群組" | ||||||
| title: "標題" | title: "標題" | ||||||
| text: "文字" | text: "文字" | ||||||
| @@ -473,7 +473,7 @@ createAccount: "建立帳戶" | |||||||
| existingAccount: "現有帳戶" | existingAccount: "現有帳戶" | ||||||
| regenerate: "再生" | regenerate: "再生" | ||||||
| fontSize: "字體大小" | fontSize: "字體大小" | ||||||
| noFollowRequests: "沒有要求跟隨您的申請" | noFollowRequests: "沒有跟隨您的請求" | ||||||
| openImageInNewTab: "於新分頁中開啟圖片" | openImageInNewTab: "於新分頁中開啟圖片" | ||||||
| dashboard: "儀表板" | dashboard: "儀表板" | ||||||
| local: "本地" | local: "本地" | ||||||
| @@ -530,8 +530,8 @@ installedDate: "安裝時間" | |||||||
| lastUsedDate: "最後上線日期" | lastUsedDate: "最後上線日期" | ||||||
| state: "狀態" | state: "狀態" | ||||||
| sort: "排序" | sort: "排序" | ||||||
| ascendingOrder: "昇冪" | ascendingOrder: "遞增" | ||||||
| descendingOrder: "降冪" | descendingOrder: "遞減" | ||||||
| scratchpad: "暫存記憶體" | scratchpad: "暫存記憶體" | ||||||
| scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" | scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" | ||||||
| output: "輸出" | output: "輸出" | ||||||
| @@ -607,7 +607,7 @@ testEmail: "測試郵件發送" | |||||||
| wordMute: "被靜音的文字" | wordMute: "被靜音的文字" | ||||||
| regexpError: "正規表達式錯誤" | regexpError: "正規表達式錯誤" | ||||||
| regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" | regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:" | ||||||
| instanceMute: "實例的靜音" | instanceMute: "被靜音的實例" | ||||||
| userSaysSomething: "{name}說了什麼" | userSaysSomething: "{name}說了什麼" | ||||||
| makeActive: "啟用" | makeActive: "啟用" | ||||||
| display: "檢視" | display: "檢視" | ||||||
| @@ -995,24 +995,24 @@ _achievements: | |||||||
|     _login3: |     _login3: | ||||||
|       title: "初學者Ⅰ" |       title: "初學者Ⅰ" | ||||||
|       description: "總登入天數為3天" |       description: "總登入天數為3天" | ||||||
|       flavor: "從今天開始,我就是Misskeyist" |       flavor: "從今天開始,我就是Misskist" | ||||||
|     _login7: |     _login7: | ||||||
|       title: "初學者ⅠⅠ" |       title: "初學者ⅠⅠ" | ||||||
|       description: "總登入天數為7天" |       description: "總登入天數為7天" | ||||||
|       flavor: "您開始習慣了嗎?" |       flavor: "您開始習慣了嗎?" | ||||||
|     _login15: |     _login15: | ||||||
|       title: "初學者III" |       title: "初學者ⅠⅠⅠ" | ||||||
|       description: "總登入天數為15天" |       description: "總登入天數為15天" | ||||||
|     _login30: |     _login30: | ||||||
|       title: "Misskeyist Ⅰ" |       title: "Misskist Ⅰ" | ||||||
|       description: "總登入天數為30天" |       description: "總登入天數為30天" | ||||||
|     _login60: |     _login60: | ||||||
|       title: "Misskeyist ⅠⅠ" |       title: "Misskist ⅠⅠ" | ||||||
|       description: "總登入天數為60天" |       description: "總登入天數為60天" | ||||||
|     _login100: |     _login100: | ||||||
|       title: "Misskeyist ⅠⅠⅠ" |       title: "Misskist ⅠⅠⅠ" | ||||||
|       description: "總登入天數為100天" |       description: "總登入天數為100天" | ||||||
|       flavor: "辣個 Misskeyist 用戶" |       flavor: "辣個 Misskist 用戶" | ||||||
|     _login200: |     _login200: | ||||||
|       title: "普通Ⅰ" |       title: "普通Ⅰ" | ||||||
|       description: "總登入天數為200天" |       description: "總登入天數為200天" | ||||||
| @@ -1089,7 +1089,7 @@ _achievements: | |||||||
|       title: "請排成一排" |       title: "請排成一排" | ||||||
|       description: "跟隨者超過300人了" |       description: "跟隨者超過300人了" | ||||||
|     _followers500: |     _followers500: | ||||||
|       title: "基站" |       title: "基地台" | ||||||
|       description: "超過500名追隨者了" |       description: "超過500名追隨者了" | ||||||
|     _followers1000: |     _followers1000: | ||||||
|       title: "影響者" |       title: "影響者" | ||||||
| @@ -1111,7 +1111,7 @@ _achievements: | |||||||
|       title: "休息一下" |       title: "休息一下" | ||||||
|       description: "用戶端啟動已超過30分鐘" |       description: "用戶端啟動已超過30分鐘" | ||||||
|     _noteDeletedWithin1min: |     _noteDeletedWithin1min: | ||||||
|       title: "現在沒有" |       title: "現在沒有了" | ||||||
|       description: "發文後1分鐘內刪文" |       description: "發文後1分鐘內刪文" | ||||||
|     _postedAtLateNight: |     _postedAtLateNight: | ||||||
|       title: "夜行性" |       title: "夜行性" | ||||||
| @@ -1181,7 +1181,7 @@ _role: | |||||||
|   name: "角色名稱" |   name: "角色名稱" | ||||||
|   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: "手動" | ||||||
| @@ -1195,8 +1195,11 @@ _role: | |||||||
|   baseRole: "基本角色" |   baseRole: "基本角色" | ||||||
|   useBaseValue: "使用基本角色的值" |   useBaseValue: "使用基本角色的值" | ||||||
|   chooseRoleToAssign: "選擇要指派的角色" |   chooseRoleToAssign: "選擇要指派的角色" | ||||||
|   canEditMembersByModerator: "允許編輯監察員的成員" |   iconUrl: "圖示的URL" | ||||||
|   descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" |   asBadge: "顯示為徽章" | ||||||
|  |   descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" | ||||||
|  |   canEditMembersByModerator: "允許編輯審查員的成員" | ||||||
|  |   descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。" | ||||||
|   priority: "優先級" |   priority: "優先級" | ||||||
|   _priority: |   _priority: | ||||||
|     low: "低" |     low: "低" | ||||||
| @@ -1233,7 +1236,7 @@ _role: | |||||||
|     or: "~或~" |     or: "~或~" | ||||||
|     not: "~否" |     not: "~否" | ||||||
| _sensitiveMediaDetection: | _sensitiveMediaDetection: | ||||||
|   description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" |   description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" | ||||||
|   sensitivity: "檢測敏感度" |   sensitivity: "檢測敏感度" | ||||||
|   sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" |   sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" | ||||||
|   setSensitiveFlagAutomatically: "設定 NSFW 旗標" |   setSensitiveFlagAutomatically: "設定 NSFW 旗標" | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"version": "13.2.4", | 	"version": "13.5.1", | ||||||
| 	"codename": "nasubi", | 	"codename": "nasubi", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
| @@ -19,7 +19,7 @@ | |||||||
| 		"start": "cd packages/backend && node ./built/boot/index.js", | 		"start": "cd packages/backend && node ./built/boot/index.js", | ||||||
| 		"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", | 		"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", | ||||||
| 		"init": "pnpm migrate", | 		"init": "pnpm migrate", | ||||||
| 		"migrate": "cd packages/backend && pnpm typeorm migration:run -d ormconfig.js", | 		"migrate": "cd packages/backend && pnpm migrate", | ||||||
| 		"migrateandstart": "pnpm migrate && pnpm start", | 		"migrateandstart": "pnpm migrate && pnpm start", | ||||||
| 		"gulp": "pnpm exec gulp build", | 		"gulp": "pnpm exec gulp build", | ||||||
| 		"watch": "pnpm dev", | 		"watch": "pnpm dev", | ||||||
| @@ -28,8 +28,8 @@ | |||||||
| 		"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", | 		"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", | ||||||
| 		"cy:run": "pnpm cypress run", | 		"cy:run": "pnpm cypress run", | ||||||
| 		"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", | 		"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", | ||||||
| 		"jest": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand", | 		"jest": "cd packages/backend && pnpm jest", | ||||||
| 		"jest-and-coverage": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand", | 		"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", | ||||||
| 		"test": "pnpm jest", | 		"test": "pnpm jest", | ||||||
| 		"test-and-coverage": "pnpm jest-and-coverage", | 		"test-and-coverage": "pnpm jest-and-coverage", | ||||||
| 		"format": "pnpm exec gulp format", | 		"format": "pnpm exec gulp format", | ||||||
| @@ -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" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								packages/backend/migration/1675404035646-cleanup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								packages/backend/migration/1675404035646-cleanup.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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`); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								packages/backend/migration/1675557528704-role-icon-badge.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1675557528704-role-icon-badge.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { DataSource } from 'typeorm'; | import { DataSource } from 'typeorm'; | ||||||
| import { loadConfig } from './built/config.js'; | import { loadConfig } from './built/config.js'; | ||||||
| import { entities } from './built/postgre.js'; | import { entities } from './built/postgres.js'; | ||||||
|  |  | ||||||
| const config = loadConfig(); | const config = loadConfig(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { DataSource } from 'typeorm'; | |||||||
| import { createRedisConnection } from '@/redis.js'; | import { createRedisConnection } from '@/redis.js'; | ||||||
| import { DI } from './di-symbols.js'; | import { DI } from './di-symbols.js'; | ||||||
| import { loadConfig } from './config.js'; | import { loadConfig } from './config.js'; | ||||||
| import { createPostgreDataSource } from './postgre.js'; | import { createPostgresDataSource } from './postgres.js'; | ||||||
| import { RepositoryModule } from './models/RepositoryModule.js'; | import { RepositoryModule } from './models/RepositoryModule.js'; | ||||||
| import type { Provider, OnApplicationShutdown } from '@nestjs/common'; | import type { Provider, OnApplicationShutdown } from '@nestjs/common'; | ||||||
|  |  | ||||||
| @@ -18,7 +18,7 @@ const $config: Provider = { | |||||||
| const $db: Provider = { | const $db: Provider = { | ||||||
| 	provide: DI.db, | 	provide: DI.db, | ||||||
| 	useFactory: async (config) => { | 	useFactory: async (config) => { | ||||||
| 		const db = createPostgreDataSource(config); | 		const db = createPostgresDataSource(config); | ||||||
| 		return await db.initialize(); | 		return await db.initialize(); | ||||||
| 	}, | 	}, | ||||||
| 	inject: [DI.config], | 	inject: [DI.config], | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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), | ||||||
| @@ -156,10 +150,6 @@ export class AntennaService implements OnApplicationShutdown { | |||||||
| 		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; | ||||||
| 	 | 	 | ||||||
| 		if (antenna.src === 'home') { | 		if (antenna.src === 'home') { | ||||||
|   | |||||||
| @@ -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; | ||||||
| @@ -150,17 +150,9 @@ export class CustomEmojiService { | |||||||
| 			if (note.renote) { | 			if (note.renote) { | ||||||
| 				emojis = emojis.concat(note.renote.emojis | 				emojis = emojis.concat(note.renote.emojis | ||||||
| 					.map(e => this.parseEmojiStr(e, note.renote!.userHost))); | 					.map(e => this.parseEmojiStr(e, note.renote!.userHost))); | ||||||
| 				if (note.renote.user) { |  | ||||||
| 					emojis = emojis.concat(note.renote.user.emojis |  | ||||||
| 						.map(e => this.parseEmojiStr(e, note.renote!.userHost))); |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 			const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis; | 			const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis; | ||||||
| 			emojis = emojis.concat(customReactions); | 			emojis = emojis.concat(customReactions); | ||||||
| 			if (note.user) { |  | ||||||
| 				emojis = emojis.concat(note.user.emojis |  | ||||||
| 					.map(e => this.parseEmojiStr(e, note.userHost))); |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 		return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[]; | 		return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[]; | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -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,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, | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
| @@ -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, | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
| @@ -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, | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
| @@ -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,14 +75,8 @@ 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) { | ||||||
| @@ -94,11 +86,11 @@ export class UserFollowingService { | |||||||
| 			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) { | ||||||
|   | |||||||
| @@ -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, | ||||||
| 	) { | 	) { | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,16 +44,25 @@ export class WebhookService implements OnApplicationShutdown { | |||||||
| 			switch (type) { | 			switch (type) { | ||||||
| 				case 'webhookCreated': | 				case 'webhookCreated': | ||||||
| 					if (body.active) { | 					if (body.active) { | ||||||
| 						this.webhooks.push(body); | 						this.webhooks.push({ | ||||||
|  | 							...body, | ||||||
|  | 							createdAt: new Date(body.createdAt), | ||||||
|  | 						}); | ||||||
| 					} | 					} | ||||||
| 					break; | 					break; | ||||||
| 				case 'webhookUpdated': | 				case 'webhookUpdated': | ||||||
| 					if (body.active) { | 					if (body.active) { | ||||||
| 						const i = this.webhooks.findIndex(a => a.id === body.id); | 						const i = this.webhooks.findIndex(a => a.id === body.id); | ||||||
| 						if (i > -1) { | 						if (i > -1) { | ||||||
| 							this.webhooks[i] = body; | 							this.webhooks[i] = { | ||||||
|  | 								...body, | ||||||
|  | 								createdAt: new Date(body.createdAt), | ||||||
|  | 							}; | ||||||
| 						} else { | 						} else { | ||||||
| 							this.webhooks.push(body); | 							this.webhooks.push({ | ||||||
|  | 								...body, | ||||||
|  | 								createdAt: new Date(body.createdAt), | ||||||
|  | 							}); | ||||||
| 						} | 						} | ||||||
| 					} else { | 					} else { | ||||||
| 						this.webhooks = this.webhooks.filter(a => a.id !== body.id); | 						this.webhooks = this.webhooks.filter(a => a.id !== body.id); | ||||||
|   | |||||||
| @@ -274,7 +274,7 @@ export class ApRendererService { | |||||||
| 		} as any; | 		} as any; | ||||||
|  |  | ||||||
| 		if (reaction.startsWith(':')) { | 		if (reaction.startsWith(':')) { | ||||||
| 			const name = reaction.replace(/:/g, ''); | 			const name = reaction.replaceAll(':', ''); | ||||||
| 			const emoji = await this.emojisRepository.findOneBy({ | 			const emoji = await this.emojisRepository.findOneBy({ | ||||||
| 				name, | 				name, | ||||||
| 				host: IsNull(), | 				host: IsNull(), | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import * as crypto from 'node:crypto'; | import * as crypto from 'node:crypto'; | ||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
|  | import jsonld from 'jsonld'; | ||||||
| import { HttpRequestService } from '@/core/HttpRequestService.js'; | import { HttpRequestService } from '@/core/HttpRequestService.js'; | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
| import { CONTEXTS } from './misc/contexts.js'; | import { CONTEXTS } from './misc/contexts.js'; | ||||||
| @@ -84,7 +85,9 @@ class LdSignature { | |||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async normalize(data: any) { | 	public async normalize(data: any) { | ||||||
| 		const customLoader = this.getLoader(); | 		const customLoader = this.getLoader(); | ||||||
| 		return 42; | 		return await jsonld.normalize(data, { | ||||||
|  | 			documentLoader: customLoader, | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@bindThis | 	@bindThis | ||||||
|   | |||||||
| @@ -48,6 +48,10 @@ export class ApImageService { | |||||||
| 			throw new Error('invalid image: url not privided'); | 			throw new Error('invalid image: url not privided'); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if (!image.url.startsWith('https://')) { | ||||||
|  | 			throw new Error('invalid image: unexpected shcema of url: ' + image.url); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		this.logger.info(`Creating the Image: ${image.url}`); | 		this.logger.info(`Creating the Image: ${image.url}`); | ||||||
|  |  | ||||||
| 		const instance = await this.metaService.fetch(); | 		const instance = await this.metaService.fetch(); | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import { forwardRef, Inject, Injectable } from '@nestjs/common'; | import { forwardRef, Inject, Injectable } from '@nestjs/common'; | ||||||
| import promiseLimit from 'promise-limit'; | import promiseLimit from 'promise-limit'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js'; | import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; | ||||||
| import type { UsersRepository } from '@/models/index.js'; |  | ||||||
| import type { Config } from '@/config.js'; | import type { Config } from '@/config.js'; | ||||||
| import type { CacheableRemoteUser } from '@/models/entities/User.js'; | import type { CacheableRemoteUser } from '@/models/entities/User.js'; | ||||||
| import type { Note } from '@/models/entities/Note.js'; | import type { Note } from '@/models/entities/Note.js'; | ||||||
| @@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js'; | |||||||
| import { StatusError } from '@/misc/status-error.js'; | import { StatusError } from '@/misc/status-error.js'; | ||||||
| import { UtilityService } from '@/core/UtilityService.js'; | import { UtilityService } from '@/core/UtilityService.js'; | ||||||
| import { MessagingService } from '@/core/MessagingService.js'; | import { MessagingService } from '@/core/MessagingService.js'; | ||||||
|  | import { bindThis } from '@/decorators.js'; | ||||||
| import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; | import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; | ||||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-imports | // eslint-disable-next-line @typescript-eslint/consistent-type-imports | ||||||
| import { ApLoggerService } from '../ApLoggerService.js'; | import { ApLoggerService } from '../ApLoggerService.js'; | ||||||
| @@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js'; | |||||||
| import { ApImageService } from './ApImageService.js'; | import { ApImageService } from './ApImageService.js'; | ||||||
| import type { Resolver } from '../ApResolverService.js'; | import type { Resolver } from '../ApResolverService.js'; | ||||||
| import type { IObject, IPost } from '../type.js'; | import type { IObject, IPost } from '../type.js'; | ||||||
| import { bindThis } from '@/decorators.js'; |  | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class ApNoteService { | export class ApNoteService { | ||||||
| @@ -134,6 +133,16 @@ export class ApNoteService { | |||||||
| 	 | 	 | ||||||
| 		this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); | 		this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); | ||||||
|  |  | ||||||
|  | 		if (note.id && !note.id.startsWith('https://')) { | ||||||
|  | 			throw new Error('unexpected shcema of note.id: ' + note.id); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		const url = getOneApHrefNullable(note.url); | ||||||
|  |  | ||||||
|  | 		if (url && !url.startsWith('https://')) { | ||||||
|  | 			throw new Error('unexpected shcema of note url: ' + url); | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
| 		this.logger.info(`Creating the Note: ${note.id}`); | 		this.logger.info(`Creating the Note: ${note.id}`); | ||||||
| 	 | 	 | ||||||
| 		// 投稿者をフェッチ | 		// 投稿者をフェッチ | ||||||
| @@ -307,7 +316,7 @@ export class ApNoteService { | |||||||
| 			apEmojis, | 			apEmojis, | ||||||
| 			poll, | 			poll, | ||||||
| 			uri: note.id, | 			uri: note.id, | ||||||
| 			url: getOneApHrefNullable(note.url), | 			url: url, | ||||||
| 		}, silent); | 		}, silent); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -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; | ||||||
| @@ -282,6 +252,12 @@ export class ApPersonService implements OnModuleInit { | |||||||
|  |  | ||||||
| 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | ||||||
|  |  | ||||||
|  | 		const url = getOneApHrefNullable(person.url); | ||||||
|  |  | ||||||
|  | 		if (url && !url.startsWith('https://')) { | ||||||
|  | 			throw new Error('unexpected shcema of person url: ' + url); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Create user | 		// Create user | ||||||
| 		let user: IRemoteUser; | 		let user: IRemoteUser; | ||||||
| 		try { | 		try { | ||||||
| @@ -313,7 +289,7 @@ export class ApPersonService implements OnModuleInit { | |||||||
| 				await transactionalEntityManager.save(new UserProfile({ | 				await transactionalEntityManager.save(new UserProfile({ | ||||||
| 					userId: user.id, | 					userId: user.id, | ||||||
| 					description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | 					description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | ||||||
| 					url: getOneApHrefNullable(person.url), | 					url: url, | ||||||
| 					fields, | 					fields, | ||||||
| 					birthday: bday ? bday[0] : null, | 					birthday: bday ? bday[0] : null, | ||||||
| 					location: person['vcard:Address'] ?? null, | 					location: person['vcard:Address'] ?? null, | ||||||
| @@ -455,6 +431,12 @@ export class ApPersonService implements OnModuleInit { | |||||||
|  |  | ||||||
| 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | 		const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); | ||||||
|  |  | ||||||
|  | 		const url = getOneApHrefNullable(person.url); | ||||||
|  |  | ||||||
|  | 		if (url && !url.startsWith('https://')) { | ||||||
|  | 			throw new Error('unexpected shcema of person url: ' + url); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		const updates = { | 		const updates = { | ||||||
| 			lastFetchedAt: new Date(), | 			lastFetchedAt: new Date(), | ||||||
| 			inbox: person.inbox, | 			inbox: person.inbox, | ||||||
| @@ -489,7 +471,7 @@ export class ApPersonService implements OnModuleInit { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		await this.userProfilesRepository.update({ userId: exist.id }, { | 		await this.userProfilesRepository.update({ userId: exist.id }, { | ||||||
| 			url: getOneApHrefNullable(person.url), | 			url: url, | ||||||
| 			fields, | 			fields, | ||||||
| 			description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | 			description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | ||||||
| 			birthday: bday ? bday[0] : null, | 			birthday: bday ? bday[0] : null, | ||||||
| @@ -540,22 +522,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)) { |  | ||||||
| 					addService(services, attachment.identifier); |  | ||||||
| 				} else { |  | ||||||
| 				fields.push({ | 				fields.push({ | ||||||
| 					name: attachment.name, | 					name: attachment.name, | ||||||
| 					value: this.mfmService.fromHtml(attachment.value), | 					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); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ type PackOptions = { | |||||||
| 	withUser?: boolean, | 	withUser?: boolean, | ||||||
| }; | }; | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
|  | import { isMimeImage } from '@/misc/is-mime-image.js'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class DriveFileEntityService { | export class DriveFileEntityService { | ||||||
| @@ -71,27 +72,42 @@ 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({ | 			if (!(mode === 'static' && file.type.startsWith('video'))) { | ||||||
| 				url: file.uri, | 				return proxiedUrl(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(file.uri); | ||||||
|  | 				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 url = file.webpublicUrl ?? file.url; | ||||||
|  |  | ||||||
| 		return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url); | 		if (mode === 'static') { | ||||||
|  | 			return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null); | ||||||
|  | 		} | ||||||
|  | 		if (mode === 'avatar') { | ||||||
|  | 			return proxiedUrl(url); | ||||||
|  | 		} | ||||||
|  | 		return url; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@bindThis | 	@bindThis | ||||||
| @@ -166,8 +182,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 +217,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); | ||||||
| 		} | 		} | ||||||
| @@ -413,8 +413,12 @@ export class UserEntityService implements OnModuleInit { | |||||||
| 				faviconUrl: instance.faviconUrl, | 				faviconUrl: instance.faviconUrl, | ||||||
| 				themeColor: instance.themeColor, | 				themeColor: instance.themeColor, | ||||||
| 			} : undefined) : undefined, | 			} : undefined) : undefined, | ||||||
| 			emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), |  | ||||||
| 			onlineStatus: this.getOnlineStatus(user), | 			onlineStatus: this.getOnlineStatus(user), | ||||||
|  | 			// パフォーマンス上の理由でローカルユーザーのみ | ||||||
|  | 			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 +426,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,10 +458,12 @@ 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, | ||||||
| 				}))), | 				}))), | ||||||
|  | 				emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  |  | ||||||
| 			...(opts.detail && isMe ? { | 			...(opts.detail && isMe ? { | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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, '냥'); | ||||||
|   | |||||||
| @@ -132,12 +132,28 @@ type NullOrUndefined<p extends Schema, T> = | |||||||
| // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection | // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection | ||||||
| // Get intersection from union  | // Get intersection from union  | ||||||
| type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; | type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; | ||||||
|  | type PartialIntersection<T> = Partial<UnionToIntersection<T>>; | ||||||
|  |  | ||||||
| // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 | // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 | ||||||
| // To get union, we use `Foo extends any ? Hoge<Foo> : never` | // To get union, we use `Foo extends any ? Hoge<Foo> : never` | ||||||
| type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never; | type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never; | ||||||
|  | type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never; | ||||||
| type ArrayUnion<T> = T extends any ? Array<T> : never; | type ArrayUnion<T> = T extends any ? Array<T> : never; | ||||||
|  |  | ||||||
|  | type ObjectSchemaTypeDef<p extends Schema> = | ||||||
|  | 	p['ref'] extends keyof typeof refs ? Packed<p['ref']> : | ||||||
|  | 	p['properties'] extends NonNullable<Obj> ? | ||||||
|  | 		p['anyOf'] extends ReadonlyArray<Schema> ? | ||||||
|  | 			ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> | ||||||
|  | 			: | ||||||
|  | 			ObjType<p['properties'], NonNullable<p['required']>[number]> | ||||||
|  | 	: | ||||||
|  | 	p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> : | ||||||
|  | 	p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : | ||||||
|  | 	any | ||||||
|  |  | ||||||
|  | type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>; | ||||||
|  |  | ||||||
| export type SchemaTypeDef<p extends Schema> = | export type SchemaTypeDef<p extends Schema> = | ||||||
| 	p['type'] extends 'null' ? null : | 	p['type'] extends 'null' ? null : | ||||||
| 	p['type'] extends 'integer' ? number : | 	p['type'] extends 'integer' ? number : | ||||||
| @@ -149,13 +165,7 @@ export type SchemaTypeDef<p extends Schema> = | |||||||
| 		string | 		string | ||||||
| 	) : | 	) : | ||||||
| 	p['type'] extends 'boolean' ? boolean : | 	p['type'] extends 'boolean' ? boolean : | ||||||
| 	p['type'] extends 'object' ? ( | 	p['type'] extends 'object' ? ObjectSchemaTypeDef<p> : | ||||||
| 		p['ref'] extends keyof typeof refs ? Packed<p['ref']> : |  | ||||||
| 		p['properties'] extends NonNullable<Obj> ? ObjType<p['properties'], NonNullable<p['required']>[number]> : |  | ||||||
| 		p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> : |  | ||||||
| 		p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> : |  | ||||||
| 		any |  | ||||||
| 	) : |  | ||||||
| 	p['type'] extends 'array' ? ( | 	p['type'] extends 'array' ? ( | ||||||
| 		p['items'] extends OfSchema ? ( | 		p['items'] extends OfSchema ? ( | ||||||
| 			p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : | 			p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] : | ||||||
| @@ -166,6 +176,7 @@ export type SchemaTypeDef<p extends Schema> = | |||||||
| 		p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] : | 		p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] : | ||||||
| 		any[] | 		any[] | ||||||
| 	) : | 	) : | ||||||
|  | 	p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> : | ||||||
| 	p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : | 	p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> : | ||||||
| 	any; | 	any; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -279,57 +279,6 @@ export class Meta { | |||||||
| 	}) | 	}) | ||||||
| 	public swPrivateKey: string | null; | 	public swPrivateKey: string | null; | ||||||
|  |  | ||||||
| 	@Column('boolean', { |  | ||||||
| 		default: false, |  | ||||||
| 	}) |  | ||||||
| 	public enableTwitterIntegration: boolean; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public twitterConsumerKey: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public twitterConsumerSecret: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('boolean', { |  | ||||||
| 		default: false, |  | ||||||
| 	}) |  | ||||||
| 	public enableGithubIntegration: boolean; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public githubClientId: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public githubClientSecret: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('boolean', { |  | ||||||
| 		default: false, |  | ||||||
| 	}) |  | ||||||
| 	public enableDiscordIntegration: boolean; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public discordClientId: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { |  | ||||||
| 		length: 128, |  | ||||||
| 		nullable: true, |  | ||||||
| 	}) |  | ||||||
| 	public discordClientSecret: string | null; |  | ||||||
|  |  | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128, | 		length: 128, | ||||||
| 		nullable: true, | 		nullable: true, | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -197,7 +197,7 @@ export const entities = [ | |||||||
| 
 | 
 | ||||||
| const log = process.env.NODE_ENV !== 'production'; | const log = process.env.NODE_ENV !== 'production'; | ||||||
| 
 | 
 | ||||||
| export function createPostgreDataSource(config: Config) { | export function createPostgresDataSource(config: Config) { | ||||||
| 	return new DataSource({ | 	return new DataSource({ | ||||||
| 		type: 'postgres', | 		type: 'postgres', | ||||||
| 		host: config.db.host, | 		host: config.db.host, | ||||||
| @@ -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 (file.fileRole === 'thumbnail') { | ||||||
| 						if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', '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, |  | ||||||
| 								498, | 						const url = new URL(`${this.config.mediaProxy}/static.webp`); | ||||||
| 								280 | 						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/')) { | 					} else if (file.mime.startsWith('video/')) { | ||||||
| 							return await this.videoProcessingService.generateVideoThumbnail(file.path); | 						image = await this.videoProcessingService.generateVideoThumbnail(file.path); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if (file.fileRole === 'webpublic') { | 				if (file.fileRole === 'webpublic') { | ||||||
| 					if (['image/svg+xml'].includes(file.mime)) { | 					if (['image/svg+xml'].includes(file.mime)) { | ||||||
| 							return this.imageProcessingService.convertToWebpStream( | 						reply.header('Cache-Control', 'max-age=31536000, immutable'); | ||||||
| 								file.path, |  | ||||||
| 								2048, | 						const url = new URL(`${this.config.mediaProxy}/svg.webp`); | ||||||
| 								2048, | 						url.searchParams.set('url', file.url); | ||||||
| 								{ ...webpDefault, lossless: true } |  | ||||||
| 							) | 						file.cleanup(); | ||||||
|  | 						return await reply.redirect(301, url.toString()); | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 					return { | 				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, | ||||||
|   | |||||||
| @@ -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'], | ||||||
| 		}, | 		}, | ||||||
| 	], | 	], | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user