Compare commits

..

168 Commits

Author SHA1 Message Date
syuilo
03744a25ed Merge branch 'develop' 2023-02-08 18:03:28 +09:00
syuilo
eac3bf8bff 13.5.0 2023-02-08 18:03:13 +09:00
syuilo
2e1fbb5b16 New Crowdin updates (#9812)
* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

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

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Chinese Traditional)
2023-02-08 18:00:45 +09:00
Masaya Suzuki
98b3517d36 package.json内のscriptsでbackendのpackage.json内のscriptsを実行する (#9833) 2023-02-08 18:00:20 +09:00
파링
dee662705e fix docker health check (#9810)
* fix(healthcheck): use default commands instead of yq

this removes yq command and uses grep and awk to get port

* fix: use correct config file

* fix: install curl in runner instead of builder

* fix: remove unused packages
2023-02-08 17:59:10 +09:00
syuilo
0da0cc80b9 fix(server): validate url from ap to improve security 2023-02-08 17:50:23 +09:00
syuilo
650187deaf perf(client): do not render custom emojis in user names
#9778
2023-02-08 17:48:02 +09:00
syuilo
2e565cac2c enhance(client): use VuePlyr
Close #9797

Co-Authored-By: Rox Squires <rox@roxsquires.gay>
2023-02-08 17:05:36 +09:00
syuilo
ac7537278c enhance(client): tweak medialist style 2023-02-08 16:54:51 +09:00
itiradi
f9a2e98831 fix(mfm): default degree not used in rotate (#9831) 2023-02-08 08:20:27 +09:00
tamaina
54f789bd55 fix(server): DriveFileEntityService.getPublicUrl調整
- 外部MediaProxyではビデオのサムネイルを生成できないので外部に投げない
- thumbnailUrlが存在しない場合、画像の場合はプロキシで圧縮させる
2023-02-07 14:24:15 +00:00
syuilo
5ac9d13516 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-07 19:59:00 +09:00
syuilo
2be1a39d13 fix(server): validate urls from ap to improve security 2023-02-07 19:58:58 +09:00
Masaya Suzuki
f3c5edc852 fix: postgre -> postgres (#9814) 2023-02-07 19:50:38 +09:00
tamaina
30704e6de8 update CHANGELOG 2023-02-06 12:13:43 +00:00
tamaina
41932ac409 MkEmojiPickerでも Fix #9598 2023-02-06 12:05:33 +00:00
tamaina
9843c596d8 disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする
Resolve #9821
Related to #6501
2023-02-06 11:29:48 +00:00
syuilo
baf65bfa69 Merge branch 'develop' 2023-02-05 20:55:51 +09:00
syuilo
6501f80fc7 13.4.0 2023-02-05 20:55:42 +09:00
syuilo
b037f6566b Add Thai to language selection
Resolve #9809
2023-02-05 20:53:40 +09:00
syuilo
0ec8ebeba3 fix(client): tweak notification style
Fix #8633
2023-02-05 20:47:27 +09:00
syuilo
af1c9251fc chore(client): add type check 2023-02-05 20:38:33 +09:00
Masaya Suzuki
4ad399c593 fix: テスト実行時のDB立ち上げコマンド修正 (#9804) 2023-02-05 20:34:22 +09:00
syuilo
55a9646f23 New Crowdin updates (#9798)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Lao)

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

* Add cp

* step分離

* step分離

* rm depends_on

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

* 末尾に移動

* Add comment

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

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

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

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

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

* Add ca-certificates

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

* インデントを揃える

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

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

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

---------

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

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

* add a comment

* ✌️

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

* fix

* thumbnail => static

* Fix #9788

* add avatar mode

* add url

* fix

* static.webp

* remove encodeURIComponent from media proxy path

* remove existance check
2023-02-04 13:38:51 +09:00
syuilo
0c12e80106 perf(server): cache blocking 2023-02-04 12:40:40 +09:00
syuilo
b7522f69e7 fix typo 2023-02-04 10:02:03 +09:00
syuilo
24705a7e39 Merge branch 'develop' 2023-02-04 09:12:26 +09:00
syuilo
8add8025a0 13.3.1 2023-02-04 09:12:18 +09:00
syuilo
32fa79d928 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-04 09:11:36 +09:00
syuilo
534be6ff25 Update CHANGELOG.md 2023-02-04 09:11:33 +09:00
syuilo
f684c07567 New Crowdin updates (#9789)
* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (German)

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

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

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

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

* Added MkMediaAudio

* Update MkMediaList.vue

* CORS checks

* Update MkMediaVideo.vue

* Update MkMediaVideo.vue

* Fixed CORS

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

---------

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

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

* Fixed the pnpm-lock.yaml

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

---------

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

Added vue-plyr as the video play

* Create node.js.yml

* Delete node.js.yml

* Added vue-plyr into pnpm-lock.yaml

* tweak

---------

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

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

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

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Ukrainian)

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

This reverts commit 8b9dc962ae.

* fix

* use deepClone instead of deepclone

* defaultStore.loaded

* fix load

* wait ready

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

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

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

* Mapを使用

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

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

---------

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

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

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Indonesian)

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

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

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

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

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

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

* docs: update CHANGELOG.md

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

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* clean up

* Implement? HttpFetchService

* ✌️

* remove node-fetch

* fix

* refactor

* fix

* gateway timeout

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

* fix

* add logger and fix url preview

* fix ip check

* enhance logger and error handling

* fix

* fix

* clean up

* Use custom fetcher for ApRequest / ApResolver

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

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

* fix

* wip????

* wip

* ✌️

* set .node-version

* clean up

* refactor

* clean up

* refactor

* refactor detectRequestType

* rename detectResponseType

* ✌️

* fix

* wip

* clean up

* no got

* remove got

* wip

* ✌️

* fix

* clean up

* remove unnnecessary const

* good cleanup

* no stream

* Revert "no stream"

This reverts commit 636f9192fc.

* fix

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

* refactor cleanup
2023-01-26 16:06:29 +09:00
syuilo
452bd6db25 tweak custom emoji handling
Close #9721
2023-01-26 15:48:12 +09:00
syuilo
f5d6b84381 chore: check emoji host 2023-01-26 14:29:28 +09:00
syuilo
34f5d81d1f Merge branch 'develop' 2023-01-26 11:40:46 +09:00
syuilo
aa8adc07aa 13.2.3 2023-01-26 11:40:36 +09:00
syuilo
d87bb807c3 tweak error screen 2023-01-26 11:39:21 +09:00
syuilo
7646d6ed47 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-26 11:33:34 +09:00
syuilo
41a6ed0de0 lint 2023-01-26 11:33:31 +09:00
syuilo
ec8074cd49 New Crowdin updates (#9724)
* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
2023-01-26 11:32:43 +09:00
syuilo
7131eb1827 fix(server): turnstile-failed: missing-input-secret
Fix #9726
2023-01-26 11:31:43 +09:00
tamaina
605b0f27e4 Merge branch 'develop' into emoji-re 2023-01-25 14:22:26 +00:00
syuilo
80d2e157f6 🎨 2023-01-25 19:49:17 +09:00
syuilo
1e3447bccb 🎨 2023-01-25 19:45:25 +09:00
syuilo
5ffa106cc1 サードパーティからも自身のロールを確認できるように
Close #9700
2023-01-25 19:34:10 +09:00
tamaina
fc641c9b96 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-25 06:21:17 +00:00
tamaina
5f49ac1b11 fix(client): アニメーションをオフに設定しても絵文字のアニメーションが止まらない
Fix #9720
2023-01-25 06:21:08 +00:00
tamaina
26fbb3a560 fix 2023-01-22 17:39:11 +00:00
tamaina
93dd0638ad better category null handling 2023-01-22 17:33:20 +00:00
tamaina
0d44129ae3 remove console.log 2023-01-22 17:20:53 +00:00
tamaina
0cffe60abc 1時間に 2023-01-22 17:14:05 +00:00
tamaina
8a6750278e ✌️ 2023-01-22 17:11:28 +00:00
tamaina
d347f0a087 wip 2023-01-22 16:07:17 +00:00
tamaina
226e0c4714 ✌️ 2023-01-22 15:17:20 +00:00
tamaina
0b2f945bb6 wip 2023-01-22 15:13:03 +00:00
tamaina
2f6c45e118 wip 2023-01-22 14:53:24 +00:00
tamaina
a5f54580a9 fix 2023-01-22 12:57:51 +00:00
tamaina
a8b19f4aa8 Merge branch 'develop' into emoji-re 2023-01-22 12:07:38 +00:00
tamaina
890564e1da refactor 2023-01-16 10:56:43 +00:00
tamaina
002f98987d fix 2023-01-16 10:51:51 +00:00
tamaina
43956f3ffb customEmojiCategories as computed 2023-01-16 10:36:29 +00:00
tamaina
f2a9194c79 ✌️ 2023-01-16 10:13:19 +00:00
tamaina
4cd70df7f4 setInterval 2023-01-16 09:52:45 +00:00
tamaina
21e4c3dfe9 wip 2023-01-16 09:39:58 +00:00
254 changed files with 5363 additions and 5222 deletions

View File

@@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

View File

@@ -114,11 +114,6 @@ id: 'aid'
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
@@ -135,6 +130,7 @@ proxyBypassHosts:
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
# Reference Implementation: https://github.com/misskey-dev/media-proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: false)

View File

@@ -16,9 +16,15 @@ files/
misskey-assets/
fluent-emojis/
.pnp.*
# .yarn関連
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml

3
.dockleignore Normal file
View File

@@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003

View File

@@ -14,6 +14,8 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
@@ -31,3 +33,5 @@ jobs:
push: true
tags: misskey/misskey:develop
labels: develop
cache-from: type=gha
cache-to: type=gha,mode=max

30
.github/workflows/dockle.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Dockle
on:
push:
branches:
- master
- develop
pull_request:
jobs:
dockle:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v3.2.0
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose.yml.example ./docker-compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
- run: |
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
echo "> ${cmd}"
eval "${cmd}"

View File

@@ -109,8 +109,12 @@ jobs:
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
- name: ALSA Env
run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
# XXX: This tries reinstalling Cypress if the binary is not cached
# Remove this when the cache issue is fixed
- name: Cypress install
run: pnpm exec cypress install
- name: Cypress run
uses: cypress-io/github-action@v4
uses: cypress-io/github-action@v5
with:
install: false
start: pnpm start:test

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
docker-compose.yml
# misskey
/build

View File

@@ -1 +1 @@
v18.12.1
v18.13.0

View File

@@ -8,6 +8,104 @@
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)
### Improvements
- リモートカスタム絵文字表示時のパフォーマンスを改善
- Default to `animation: false` when prefers-reduced-motion is set
- リアクション履歴が公開なら、ログインしていなくても表示できるように
- tweak blur setting
- tweak custom emoji cache
### Bugfixes
- fix aggregation of retention
- ダッシュボードでオンラインユーザー数が表示されない問題を修正
- フォロー申請・フォローのボタンが、通知から消えている問題を修正
## 13.2.3 (2023/01/26)
### Improvements
- カスタム絵文字の更新をリアルタイムで反映するように
### Bugfixes
- turnstile-failed: missing-input-secret
## 13.2.2 (2023/01/25)
### Improvements

View File

@@ -44,7 +44,7 @@ Thank you for your PR! Before creating a PR, please check the following:
- Check if there are any documents that need to be created or updated due to this change.
- 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.
- 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.
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
```
yarn dev
pnpm dev
```
command.
@@ -112,7 +112,7 @@ command.
- Service Worker is watched by esbuild.
## Testing
- Test codes are located in [`/test`](/test).
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
### Run test
Create a config file.
@@ -121,18 +121,18 @@ cp .github/misskey/test.yml .config/
```
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`.
Run all test.
```
yarn test
pnpm test
```
#### Run specify test
```
yarn jest -- foo.ts
pnpm jest -- foo.ts
```
### e2e tests
@@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## 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を使用する
#6441
@@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
### Migration作成方法
packages/backendで:
```sh
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name>
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
```
- 生成後、ファイルをmigration下に移してください

View File

@@ -2,8 +2,12 @@ ARG NODE_VERSION=18.13.0-bullseye
FROM node:${NODE_VERSION} AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -yqq --no-install-recommends \
build-essential
RUN corepack enable
@@ -16,7 +20,8 @@ COPY ["packages/backend/package.json", "./packages/backend/"]
COPY ["packages/frontend/package.json", "./packages/frontend/"]
COPY ["packages/sw/package.json", "./packages/sw/"]
RUN pnpm i --frozen-lockfile
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output
COPY . ./
@@ -24,20 +29,25 @@ ARG NODE_ENV=production
RUN git submodule update --init
RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner
ARG UID="991"
ARG GID="991"
RUN apt-get update \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg tini \
&& apt-get -y clean \
&& rm -rf /var/lib/apt/lists/* \
ffmpeg tini curl \
&& corepack enable \
&& 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
WORKDIR /misskey
@@ -51,5 +61,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue
COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production
HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "migrateandstart"]

View File

@@ -24,6 +24,8 @@
---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
</div>
<div>

View File

@@ -133,11 +133,6 @@ id: "aid"
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128

4
healthcheck.sh Normal file
View 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}"

View File

@@ -68,7 +68,7 @@ export: "Export"
files: "Dateien"
download: "Herunterladen"
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."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen"
@@ -94,7 +94,7 @@ defaultNoteVisibility: "Standardsichtbarkeit"
follow: "Folgen"
followRequest: "Follow-Anfrage senden"
followRequests: "Follow-Anfragen"
unfollow: "Nicht mehr folgen"
unfollow: "Entfolgen"
followRequestPending: "Follow-Anfrage ausstehend"
enterEmoji: "Gib ein Emoji ein"
renote: "Renote"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Rollenvorlage"
useBaseValue: "Wert der Rollenvorlage verwenden"
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"
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"

View File

@@ -68,7 +68,7 @@ export: "Export"
files: "Files"
download: "Download"
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."
importRequested: "You've requested an import. This may take a while."
lists: "Lists"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Role template"
useBaseValue: "Use role template value"
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"
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"

View File

@@ -509,7 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
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"
sound: "Sonidos"
listen: "Escuchar"
@@ -918,17 +918,326 @@ tools: "Utilidades"
cannotLoad: "No se puede cargar."
numberOfProfileView: "Número de vistas de perfil"
like: "¡Muy bien!"
unlike: "Quitar 'me gusta'"
numberOfLikes: "Cantidad de 'Me gusta'"
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"
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:
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:
low: "Baja"
middle: "Mediano"
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:
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."
sensitivity: "Sensibilidad de detección"
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 la detección"
sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)."
setSensitiveFlagAutomatically: "Marcar como NSFW"
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"
serverMetric: "Estadísticas del servidor"
aiscript: "Consola de AiScript"
aiscriptApp: "Aplicación AiScript"
aichan: "indigo"
userList: "Lista de usuarios"
_userList:
chooseList: "Seleccione una lista"
clicker: "Cliqueador"
_cw:
hide: "Ocultar"
show: "Ver más"
@@ -1434,7 +1745,16 @@ _timelines:
social: "Social"
global: "Global"
_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"
my: "Mis guiones"
liked: "Guiones que te gustaron"
featured: "Popular"
title: "Título"
script: "Script"
@@ -1507,6 +1827,7 @@ _notification:
pollEnded: "Estan disponibles los resultados de la encuesta"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
_types:
all: "Todo"
follow: "Siguiendo"

View File

@@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..."
ok: "OK"
gotIt: "Saya mengerti"
cancel: "Batalkan"
noThankYou: "Tidak sekarang."
enterUsername: "Masukkan nama pengguna"
renotedBy: "direnote oleh {user}"
noNotes: "Tidak ada catatan"
@@ -206,6 +207,7 @@ done: "Selesai"
processing: "Memproses"
preview: "Pratinjau"
default: "Bawaan"
defaultValueIs: "Bawaan: {value}"
noCustomEmojis: "Tidak ada emoji kustom"
noJobs: "Tidak ada kerja"
federating: "memfederasi"
@@ -349,6 +351,8 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Nyalakan reCAPTCHA"
recaptchaSiteKey: "Site key"
recaptchaSecretKey: "Secret Key"
turnstile: "Turnstile"
enableTurnstile: "Nyalakan Turnstile"
turnstileSiteKey: "Site 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."
@@ -454,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna"
groupInvited: "Telah diundang ke grup"
aboutX: "Tentang {x}"
emojiStyle: "Gaya emoji"
native: "Native"
disableDrawer: "Jangan gunakan menu bergaya laci"
youHaveNoGroups: "Kamu tidak memiliki grup"
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
@@ -857,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
cropImage: "potong gambar"
cropImageAsk: "Ingin memotong gambar?"
file: "Berkas"
recentNHours: "{n} jam terakhir"
recentNDays: "{n} hari terakhir"
noEmailServerWarning: "Mail Server tidak disetel."
thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan."
recommended: "Disarankan"
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"
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?"
lastActiveDate: "Terakhir digunakan"
statusbar: "Bilah status"
@@ -870,20 +886,189 @@ colored: "Diwarnai"
refreshInterval: "Jeda pembaharuan"
label: "Label"
type: "Tipe"
speed: "Kecepatan"
slow: "Lambat"
fast: "Cepat"
sensitiveMediaDetection: "Deteksi media NSFW"
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"
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"
unlike: "Tidak Suka"
numberOfLikes: "Jumlah yang disukai"
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"
_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:
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:
low: "Rendah"
middle: "Sedang"
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:
used: "Alamat surel ini telah digunakan"
format: "Format tidak valid."
@@ -1167,6 +1352,7 @@ _tutorial:
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_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
step8_3: "Kamu dapat mengganti pengaturan ini nanti."
_2fa:
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
registerDevice: "Daftarkan perangkat baru"
@@ -1241,10 +1427,13 @@ _widgets:
trends: "Tren"
clock: "Jam"
rss: "Pembaca RSS"
rssTicker: "RSS-Ticker"
activity: "Aktivitas"
photos: "Foto"
digitalClock: "Jam digital"
unixClock: "Jam UNIX"
federation: "Federasi"
instanceCloud: "Instansi awan"
postForm: "Buat catatan"
slideshow: "Slideshow"
button: "Tombol"
@@ -1254,8 +1443,10 @@ _widgets:
aiscript: "Konsol AiScript"
aiscriptApp: "Aplikasi AiScript"
aichan: "Ai"
userList: "Daftar pengguna"
_userList:
chooseList: "Pilih daftar"
clicker: "Pengeklik"
_cw:
hide: "Sembunyikan"
show: "Lihat konten"
@@ -1319,6 +1510,7 @@ _profile:
changeBanner: "Ubah header"
_exportOrImport:
allNotes: "Semua catatan"
favoritedNotes: "Catatan favorit"
followingList: "Ikuti"
muteList: "Bisukan"
blockingList: "Blokir"
@@ -1437,7 +1629,9 @@ _notification:
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
youWereInvitedToGroup: "Telah diundang ke grup"
pollEnded: "Hasil Kuesioner telah keluar"
unreadAntennaNote: "Antena {name}"
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
achievementEarned: "Pencapaian didapatkan"
_types:
all: "Semua"
follow: "Ikuti"
@@ -1459,6 +1653,7 @@ _deck:
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
columnAlign: "Luruskan kolom"
addColumn: "Tambahkan kolom"
configureColumn: "Atur kolom"
swapLeft: "Pindah ke kiri"
swapRight: "Pindah ke kanan"
swapUp: "Pindah ke atas"
@@ -1466,6 +1661,11 @@ _deck:
stackLeft: "Tumpukkan di kolom kiri"
popRight: "Keluarkan di kanan"
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:
main: "Utama"
widgets: "Widget"

View File

@@ -34,6 +34,7 @@ const languages = [
'pt-PT',
'ru-RU',
'sk-SK',
'th-TH',
'ug-CN',
'uk-UA',
'vi-VN',

View File

@@ -1044,7 +1044,7 @@ _achievements:
flavor: "Grazie per aver usato Misskey!"
_noteClipped1:
title: "Devo clippare!"
description: "Ho raccolto in Clip la prima Nota"
description: "Hai raccolto la tua prima Nota in una Clip"
_noteFavorited1:
title: "Guarda le stelle"
description: "Aggiungi una Nota ai preferiti per la prima volta"
@@ -1080,7 +1080,7 @@ _achievements:
title: "Follow me!"
description: "Hai ottenuto 10 profili Follower"
_followers50:
title: "Follower a frotte"
title: "Un gregge di Follower"
description: "Hai ottenuto 50 Follower"
_followers100:
title: "Popolare"
@@ -1108,7 +1108,7 @@ _achievements:
title: "Caccia al tesoro"
description: "Hai trovato un tesoro nascosto"
_client30min:
title: "Piccola pausa"
title: "Piccola grande pausa"
description: "Hai passato più di 30 minuti su Misskey"
_noteDeletedWithin1min:
title: "Ooops!"
@@ -1134,7 +1134,7 @@ _achievements:
title: "Hello, world!"
description: "Hai scritto «Hello world» nel blocco appunti"
_open3windows:
title: "Finestrato"
title: "Apri le finestre!"
description: "Hai aperto almeno 3 finestre contemporaneamente"
_driveFolderCircularReference:
title: "Riferimento circolare"
@@ -1170,7 +1170,7 @@ _achievements:
_cookieClicked:
title: "Clicca il biscotto"
description: "Hai giocato a cliccare il cookie"
flavor: "Hai autorizzato i cookie?"
flavor: "È il sito giusto?"
_brainDiver:
title: "Brain Diver"
description: "Pubblica un link a Brain Diver"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base"
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"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità"

View File

@@ -1148,7 +1148,7 @@ _achievements:
description: "ここをクリックした"
_justPlainLucky:
title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得"
description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo:
title: "神様コンプレックス"
description: "名前を syuilo に設定した"
@@ -1184,7 +1184,7 @@ _role:
description: "ロールの説明"
permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット"
assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル"
conditional: "コンディショナル"
@@ -1197,6 +1197,9 @@ _role:
baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"

72
locales/lo-LA.yml Normal file
View 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: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"

View File

@@ -22,7 +22,7 @@ instance: "Инстанс"
settings: "Настройки"
basicSettings: "Основные настройки"
otherSettings: "Прочие настройки"
openInWindow: "Открывать в плавающих окнах"
openInWindow: "Открыть в плавающем окне"
profile: "Профиль"
timeline: "Лента"
noAccountDescription: "Пользователь ничего не написал про себя"
@@ -273,7 +273,7 @@ light: "Светлый"
dark: "Тёмный"
lightThemes: "Светлые темы"
darkThemes: "Тёмные темы"
syncDeviceDarkMode: "Синхронизировать с темным режимом устройства"
syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
drive: "Диск"
fileName: "Имя файла"
selectFile: "Выберите файл"
@@ -456,6 +456,7 @@ uiLanguage: "Язык интерфейса"
groupInvited: "Приглашение в группу"
aboutX: "Описание {x}"
emojiStyle: "Стиль эмодзи"
native: "Системные"
disableDrawer: "Не использовать выдвижные меню"
youHaveNoGroups: "У вас нет ни одной группы"
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
@@ -603,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS."
testEmail: "Проверка доставки электронной почты"
wordMute: "Скрытие слов"
regexpError: "Ошибка в регулярном выражении"
regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
instanceMute: "Глушение инстансов"
userSaysSomething: "{name} что-то сообщает"
makeActive: "Активировать"
@@ -804,7 +806,7 @@ translate: "Перевод"
translatedFrom: "Перевод. Язык оригинала — {x}"
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
aiChanMode: "ИИ режим"
aiChanMode: "Режим Ай"
keepCw: "Сохраняйте Предупреждения о содержимом"
pubSub: "Учётные записи Pub/Sub"
lastCommunication: "Последнее сообщение"
@@ -821,8 +823,8 @@ manageAccounts: "Управление аккаунтом"
makeReactionsPublic: "Опубликовать список реакций"
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
classic: "Классика"
muteThread: "Заглушить цепочку"
unmuteThread: "Отменить глушение цепочки"
muteThread: "Скрыть цепочку"
unmuteThread: "Отменить сокрытие цепочки"
ffVisibility: "Видимость подписок и подписчиков"
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
continueThread: "Показать следующие ответы"
@@ -891,6 +893,7 @@ cannotUploadBecauseNoFreeSpace: "Файл не может быть загруж
beta: "Бета"
enableAutoSensitive: "Автоматическое определение NSFW"
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта."
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
navbar: "Панель навигации"
shuffle: "Перемешать"
account: "Учётные записи"
@@ -1096,6 +1099,9 @@ _achievements:
title: "Я люблю Misskey"
description: "Написана заметка «I ❤ #Misskey»"
flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков"
_foundTreasure:
title: "Охота за сокровищами"
description: "Найдено спрятанное сокровище"
_client30min:
title: "Перерыв на обед"
description: "Прошло 30 минут с момента запуска клиента"
@@ -1116,6 +1122,9 @@ _achievements:
_htl20npm:
title: "В потоке"
description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)"
_viewInstanceChart:
title: "Аналитик"
description: "Просмотрены статистические диаграммы инстанса"
_outputHelloWorldOnScratchpad:
title: "Привет, мир!"
description: "Выведен текст «hello world» в Когтеточке"
@@ -1189,7 +1198,34 @@ _role:
middle: "Средне"
high: "Высокий"
_options:
gtlAvailable: "Может просматривать глобальную ленту"
ltlAvailable: "Может просматривать местную ленту"
canPublicNote: "Может публиковать общедоступные заметки"
canInvite: "Может создавать пригласительные коды"
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:
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
setSensitiveFlagAutomatically: "Установить флаг NSFW"
@@ -1237,10 +1273,23 @@ _plugin:
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
manage: "Управление расширениями"
_preferencesBackups:
saveConfirm: "Сохранить бэкап как {name}?"
deleteConfirm: "Удалить резервную копию {name}?"
renameConfirm: "Переименовать резервную копию с \"{old}\" на \"{new}\"?"
noBackups: "Резервной копии не существует. Вы можете создать резервную копию в настройках на этом инстансе с помощью \"Создать новую резервную копию\"."
list: "Существующие резервные копии"
saveNew: "Создать резервную копию"
loadFile: "Прочесть из файла"
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:
scope: "Область"
key: "Ключ"
@@ -1324,6 +1373,8 @@ _mfm:
sparkleDescription: "Добавляет эффект искрящихся частиц."
rotate: "Повернуть"
rotateDescription: "Поворачивает на заданный угол."
plain: "Буквально"
plainDescription: "MFM внутри отключается, и текст отображается как есть"
_instanceTicker:
none: "Не показывать"
remote: "Только для других сайтов"
@@ -1353,12 +1404,14 @@ _wordMute:
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
soft: "Мягкий"
hard: "Жёсткий"
soft: "Мягко"
hard: "Жёстко"
mutedNotes: "Скрытые заметки"
_instanceMute:
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
title: "Скрывает заметки с заданных инстансов."
heading: "Список заглушенных инстансов"
heading: "Список скрытых инстансов"
_theme:
explore: "Обзор"
install: "Установить тему"
@@ -1479,12 +1532,16 @@ _tutorial:
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
step7_3: "Приятно вам провести время с Misskey🚀"
step8_1: "Ах, да, не хотите ли включить push-уведомления?"
step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey."
step8_3: "Эту настройку вы всегда сможете поменять"
_2fa:
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
registerDevice: "Зарегистрируйте ваше устройство"
registerKey: "Зарегистрировать ключ"
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
step3: "И наконец, введите код, который покажет приложение."
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
@@ -1501,7 +1558,7 @@ _permissions:
"write:following": "Изменять спискок подписок"
"read:messaging": "Смотреть сообщения"
"write:messaging": "Писать и удалять сообщения"
"read:mutes": "Смотреть спискок скрытых пользователей"
"read:mutes": "Смотреть список скрытых пользователей"
"write:mutes": "Изменять список скрытых пользователей"
"write:notes": "Писать и удалять заметки"
"read:notifications": "Смотреть уведомления"
@@ -1552,10 +1609,13 @@ _widgets:
trends: "Актуальное"
clock: "Часы"
rss: "Просмотр RSS"
rssTicker: "Бегущая строка RSS"
activity: "Активность"
photos: "Фото"
digitalClock: "Цифровые часы"
unixClock: "Часы UNIX"
federation: "Федерация"
instanceCloud: "Облако инстансов"
postForm: "Форма отправки"
slideshow: "Показ слайдов"
button: "Кнопка"
@@ -1563,9 +1623,12 @@ _widgets:
jobQueue: "Очередь заданий"
serverMetric: "Показатели сервера"
aiscript: "Консоль AiScript"
aiscriptApp: "Приложение на AiScript"
aichan: "Ай"
userList: "Список аккаунтов"
_userList:
chooseList: "Выберите список"
clicker: "Счётчик щелчков"
_cw:
hide: "Спрятать"
show: "Показать еще"
@@ -1628,12 +1691,13 @@ _profile:
changeAvatar: "Поменять аватар"
changeBanner: "Поменять изображение в шапке"
_exportOrImport:
allNotes: "Все записи\n"
allNotes: "Все заметки\n"
favoritedNotes: "Избранное"
followingList: "Подписки"
muteList: "Скрытые"
blockingList: "Заблокированные"
userLists: "Списки"
excludeMutingUsers: "За исключением заглушенных пользователей"
excludeMutingUsers: "За исключением скрытых пользователей"
excludeInactiveUsers: "Без неактивных учётных записей"
_charts:
federation: "Федерация"
@@ -1737,6 +1801,8 @@ _notification:
youReceivedFollowRequest: "У вас новый запрос на подписку."
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
youWereInvitedToGroup: "Вы приглашены в группу."
pollEnded: "Подведены окончательные итоги опроса"
emptyPushNotificationMessage: "Обновлены push-уведомления"
achievementEarned: "Получено достижение"
_types:
all: "Все"
@@ -1746,11 +1812,13 @@ _notification:
renote: "Репосты"
quote: "Цитаты"
reaction: "Реакции"
pollEnded: "Окончания опросов"
receiveFollowRequest: "Получен запрос на подписку"
followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Приглашение в группы"
app: "Уведомления из приложений"
_actions:
followBack: "отвечает взаимной подпиской"
reply: "Ответить"
renote: "Репост"
_deck:
@@ -1764,7 +1832,12 @@ _deck:
swapDown: "Переставить ниже"
stackLeft: "В столбик влево"
popRight: "Из столбика вправо"
profile: "Профиль"
profile: "Расстановка"
newProfile: "Новая расстановка"
deleteProfile: "Удаление расстановки"
introduction: "Создайте идеальный интерфейс расставляя колонки как угодно"
introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана."
widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки."
_columns:
main: "Основная"
widgets: "Виджеты"

View File

@@ -944,48 +944,236 @@ _achievements:
_types:
_notes1:
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:
title: "บุคคลที่เป็นที่นิยม"
description: "ได้รับ 100 ผู้ติดตาม"
_followers300:
title: "กรุณาสร้างบรรทัดเดียวนะคะ"
description: "ได้รับ 300 คนผู้ติดตาม"
_followers500:
title: "เสาสัญญาณ"
description: "ได้รับ 500 คนผู้ติดตาม"
_followers1000:
title: "ผู้ทรงอิทธิพล"
description: "ได้รับ 1,000 ผู้ติดตาม"
_collectAchievements30:
title: "นักสะสมความสำเร็จ"
description: "ได้รับความสำเร็จ 30 ครั้ง"
_viewAchievements3min:
title: "ชอบบรรลุผลสําเร็จ"
description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
_iLoveMisskey:
title: "ฉันรัก Misskey"
description: "โพสต์ \"I ❤ #Misskey\""
flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!"
_foundTreasure:
title: "ล่าสมบัติ"
description: "คุณพบสมบัติที่ซ่อนอยู่"
_client30min:
title: "พักผ่อนสักหน่อย"
description: "ใช้เวลา 30 นาทีบน Misskey"
_noteDeletedWithin1min:
title: "ไม่เป็นไร"
description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์"
_postedAtLateNight:
title: "กลางคืน"
description: "โพสต์โน้ตตอนดึกๆ"
flavor: "ได้เวลาเข้านอนแล้วนะ"
_postedAt0min0sec:
title: "นาฬิกาพูดได้"
description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น."
flavor: "คลิก คลิก คลิก แกล๊งๆ"
_selfQuote:
title: "อ้างอิงตนเอง"
description: "อ้างโน้ตย่อของคุณเอง"
_htl20npm:
title: "ไทม์ไลน์ไหล"
description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)"
_viewInstanceChart:
title: "วิเคราะห์"
description: "ดูแผนภูมิอินสแตนซ์ของคุณ"
_outputHelloWorldOnScratchpad:
title: "หวัดดีชาวโลก!"
description: "เอาพุต \"hello world\" ใน Scratchpad"
_open3windows:
title: "มัลติวินโดว์"
description: "มีการเปิดหน้าต่างอย่างน้อย 3 หน้าต่างพร้อมกัน"
_driveFolderCircularReference:
title: "อ้างอิงวงจร"
description: "พยายามสร้างโฟลเดอร์ที่ซ้อนกันแบบวนซ้ำในไดรฟ์"
_reactWithoutRead:
title: "คุณอ่านมันจริงๆหรือเปล่า?"
description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์"
_clickedClickHere:
title: "คลิ๊กที่นี่"
description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
_passedSinceAccountCreated1:
title: "ครบรอบหนึ่งปี"
description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated2:
title: "ครบรอบสองปี"
description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_passedSinceAccountCreated3:
title: "ครบรอบสามปี"
description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
_loggedInOnBirthday:
title: "สุขสันต์วันเกิด"
description: "เข้าสู่ระบบในวันเกิดของคุณ"
_loggedInOnNewYearsDay:
title: "สวัสดีปีใหม่!"
description: "เข้าสู่ระบบในวันแรกของปี"
flavor: "อีกปีที่ยอดเยี่ยมในโอกาสนี้เลย"
_cookieClicked:
title: "เกมที่คุณคลิกที่คุกกี้"
description: "คลิกคุกกี้"
flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?"
_brainDiver:
title: "Brain Diver"
description: "โพสต์ลิงก์ไปยัง Brain Diver"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "บทบาทใหม่"
@@ -994,7 +1182,7 @@ _role:
description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย"
assignTarget: "มอบหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง"
conditional: "มีเงื่อนไข"
@@ -1007,6 +1195,9 @@ _role:
baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"

View File

@@ -529,7 +529,7 @@ state: "Стан"
sort: "Сортування"
ascendingOrder: "За зростанням"
descendingOrder: "За спаданням"
scratchpad: "Чернетка"
scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
output: "Вихід"
script: "Скрипт"
@@ -688,7 +688,7 @@ pageLikesCount: "Кількість отриманих вподобань сто
pageLikedCount: "Кількість вподобаних сторінок"
contact: "Контакт"
useSystemFont: "Використовувати стандартний шрифт системи"
clips: "Добірка"
clips: "Добірки"
experimentalFeatures: "Експериментальні функції"
developer: "Розробник"
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
@@ -904,7 +904,7 @@ _achievements:
earnedAt: "Відкрито"
_types:
_notes1:
title: "налаштовую свій msky"
title: "Привіт, Misskey!"
description: "Перша нотатка"
flavor: "Приємного часу з Misskey!"
_notes10:
@@ -956,9 +956,11 @@ _achievements:
_login3:
title: "Новачок I"
description: "3 дні користування загально"
flavor: "Відсьогодні називайте мене \"Місскіст\""
_login7:
title: "Новачок II"
description: "7 днів користування загально"
flavor: "Ви звикли до цього?"
_login15:
title: "Новачок III"
description: "15 днів користування загально"
@@ -971,6 +973,7 @@ _achievements:
_login100:
title: "Міскієць III"
description: "100 днів користування загально"
flavor: "Цей юзер лютий місскіст"
_login200:
title: "Завсідник I"
description: "200 днів користування загально"
@@ -983,6 +986,7 @@ _achievements:
_login500:
title: "Ветеран I"
description: "500 днів користування загально"
flavor: "Meine Kameraden, ich liebe sie, die Notizen."
_login600:
title: "Ветеран II"
description: "600 днів користування загально"
@@ -990,13 +994,35 @@ _achievements:
title: "Ветеран III"
description: "700 днів користування загально"
_login800:
title: "Майстер нотаток I"
description: "800 днів користування загально"
_login900:
title: "Майстер нотаток II"
description: "900 днів користування загально"
_login1000:
title: "Майстер нотаток III"
description: "1000 днів користування загально"
flavor: "Дякуємо, що користуєтеся Misskey!"
_noteClipped1:
title: "Не можна не зберегти"
description: "Перша нотатка у добірці"
_noteFavorited1:
title: "Дивитися на зірки"
_myNoteFavorited1:
title: "У пошуках зірок"
_profileFilled:
title: "Повна готовність"
description: "Профіль заповнено"
_markedAsCat:
title: "Я кіт"
description: "Позначено як акаунт кота"
flavor: "Я дам тобі ім'я пізніше"
_following1:
title: "Перша підписка"
_following10:
title: "Продовжуй, продовжуй"
_following50:
title: "Багато друзів"
description: "Кількість підписок сягнула 50"
_following100:
title: "100 друзів"
@@ -1013,19 +1039,81 @@ _achievements:
_followers50:
description: "Кількість підписників досягла 50"
_followers100:
title: "Популярна особа"
description: "Кількість підписників досягла 100"
_followers300:
title: "Ставайте в чергу"
description: "Кількість підписників досягла 300"
_followers500:
title: "Радіовежа"
description: "Кількість підписників досягла 500"
_followers1000:
title: "Інфлюенсер"
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:
title: "Перша річниця"
description: "Минув рік з моменту створення акаунта"
_passedSinceAccountCreated2:
title: "Друга річниця"
description: "Минуло 2 роки з моменту створення акаунта"
_passedSinceAccountCreated3:
title: "Третя річниця"
description: "Минуло 3 роки з моменту створення акаунта"
_loggedInOnBirthday:
title: "З Днем народження!"
description: "Увійти у свій день народження"
_loggedInOnNewYearsDay:
title: "З Новим роком!"
description: "Увійшли в перший день року"
_brainDiver:
title: "Brain Diver"
description: "Відправити посилання на \"Brain Diver\""
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
priority: "Пріоритет"
@@ -1294,12 +1382,12 @@ _tutorial:
step1_1: "Ласкаво просимо!"
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\""
step4_1: "Ви розмістили свій перший запис?"
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
@@ -1563,6 +1651,7 @@ _notification:
youReceivedFollowRequest: "Ви отримали запит на підписку"
yourFollowRequestAccepted: "Запит на підписку прийнято"
youWereInvitedToGroup: "Запрошення до групи"
achievementEarned: "Досягнення відкрито"
_types:
all: "Все"
follow: "Підписки"

View File

@@ -995,52 +995,170 @@ _achievements:
_login3:
title: "初学者 I"
description: "连续登录3天"
flavor: "今天开始我就是Misskist"
_login7:
title: "初学者 II"
description: "连续登录7天"
flavor: "您开始习惯了吗?"
_login15:
title: "初学者 III"
description: "连续登录15天"
_login30:
title: "Misskist "
description: "连续登录30天"
_login60:
title: "Misskist Ⅱ"
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:
title: "帖子大师Ⅲ"
description: "总登录天数1000天"
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: "我的朋友很多"
description: "关注超过100人"
_following300:
title: "朋友成群"
description: "关注数超过300"
_followers1:
title: "最初的关注者"
description: "第一次被关注"
_followers10:
title: "关注我吧!"
description: "拥有超过10名关注者"
_followers50:
title: "三五成群"
description: "拥有超过50名关注者"
_followers100:
title: "胜友如云"
description: "拥有超过100名关注者"
_followers300:
title: "排列成行"
description: "拥有超过300名关注者"
_followers500:
title: "信号塔"
description: "拥有超过500名关注者"
_followers1000:
title: "大影响家"
description: "拥有超过1000名关注者"
_collectAchievements30:
title: "成就收藏家"
description: "获得超过30个成就"
_viewAchievements3min:
title: "成就爱好者"
description: "盯着成就看三分钟"
_iLoveMisskey:
title: "I Love Misskey"
description: "发布\"I ❤ #Misskey\"帖子"
flavor: "感谢您使用 Misskey by 开发团队"
_foundTreasure:
title: "寻宝"
description: "发现了隐藏的宝藏"
_client30min:
title: "休息一下!"
description: "启动客户端超过30分钟"
_noteDeletedWithin1min:
title: "无话可说"
description: "发帖后一分钟内就将其删除"
_postedAtLateNight:
title: "夜行者"
title: "夜猫子"
description: "深夜发布帖子"
flavor: "差不多该去睡了喔。"
_postedAt0min0sec:
title: "报时"
description: "在0点发布一篇帖子"
flavor: "嘣 嘣 嘣 Biu——"
_selfQuote:
title: "自我提及"
description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
description: "在首页时间线的流速超过20npm"
_viewInstanceChart:
title: "分析师"
description: "查看了实例信息中的图表"
_outputHelloWorldOnScratchpad:
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:
title: "一周年"
description: "账户创建时间超过1年"
_passedSinceAccountCreated2:
title: "二周年"
description: "账户创建时间超过2年"
_passedSinceAccountCreated3:
title: "三周年"
description: "账户创建时间超过3年"
_loggedInOnBirthday:
title: "生日快乐"
@@ -1048,6 +1166,15 @@ _achievements:
_loggedInOnNewYearsDay:
title: "恭贺新禧"
description: "在元旦登入"
flavor: "今年也请对本实例多多指教!"
_cookieClicked:
title: "点击饼干小游戏"
description: "点击了可疑的饼干"
flavor: "是不是软件有问题?"
_brainDiver:
title: "Brain Diver"
description: "发布了包含Brain Diver链接的帖子"
flavor: "Misskey-Misskey La-Tu-Ma"
_role:
new: "创建角色"
edit: "编辑角色"
@@ -1068,6 +1195,9 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级"
@@ -1566,7 +1696,7 @@ _profile:
name: "昵称"
username: "用户名"
description: "个人简介"
youCanIncludeHashtags: "可以包含一个哈希标签。"
youCanIncludeHashtags: "可以在个人简介中包含一个#标签。"
metadata: "附加信息"
metadataEdit: "附加信息编辑"
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"

View File

@@ -240,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
resetAreYouSure: "確定要重設嗎?"
saved: "已儲存"
messaging: "傳送訊息"
messaging: "聊天"
upload: "上傳"
keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時瀏覽器會在上傳時生成一張用於web發布的圖片。"
@@ -326,15 +326,15 @@ connectService: "己連結"
disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
inMb: "以Mbps為單位"
iconUrl: "圖URL"
bannerUrl: "橫幅圖URL"
iconUrl: "圖URL"
bannerUrl: "橫幅圖URL"
backgroundImageUrl: "背景圖片的來源網址 "
basicInfo: "基本資訊"
pinnedUsers: "置頂用戶"
@@ -373,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶"
notesAndReplies: "貼文與回覆"
withFiles: "附件"
silence: "禁言"
silenceConfirm: "確定要禁言此用戶嗎?"
unsilence: "解除禁言"
silenceConfirm: "確定要靜音此使用者嗎?"
unsilence: "解除靜音"
unsilenceConfirm: "確定要解除禁言嗎?"
popularUsers: "熱門使用者"
recentlyUpdatedUsers: "最近發文的使用者"
@@ -383,14 +383,14 @@ recentlyDiscoveredUsers: "最近發現的使用者"
exploreUsersCount: "有{count}個使用者"
exploreFediverse: "探索聯邦世界"
popularTags: "熱門標籤"
userList: "清單"
about: "資訊"
userList: "使用者清單"
about: "關於"
aboutMisskey: "關於 Misskey"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "監察員"
moderation: "監察"
moderator: "審查員"
moderation: "審查"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@@ -421,7 +421,7 @@ invites: "邀請"
groupName: "群組名稱"
members: "成員"
transfer: "轉讓"
messagingWithUser: "傳送訊息給其他使用者"
messagingWithUser: "其他使用者聊天"
messagingWithGroup: "發送訊息至群組"
title: "標題"
text: "文字"
@@ -473,7 +473,7 @@ createAccount: "建立帳戶"
existingAccount: "現有帳戶"
regenerate: "再生"
fontSize: "字體大小"
noFollowRequests: "沒有要求跟隨您的請"
noFollowRequests: "沒有跟隨您的請"
openImageInNewTab: "於新分頁中開啟圖片"
dashboard: "儀表板"
local: "本地"
@@ -530,8 +530,8 @@ installedDate: "安裝時間"
lastUsedDate: "最後上線日期"
state: "狀態"
sort: "排序"
ascendingOrder: "昇冪"
descendingOrder: "降冪"
ascendingOrder: "遞增"
descendingOrder: "遞減"
scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出"
@@ -607,7 +607,7 @@ testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音"
instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼"
makeActive: "啟用"
display: "檢視"
@@ -995,24 +995,24 @@ _achievements:
_login3:
title: "初學者Ⅰ"
description: "總登入天數為3天"
flavor: "從今天開始我就是Misskeyist"
flavor: "從今天開始我就是Misskist"
_login7:
title: "初學者ⅠⅠ"
description: "總登入天數為7天"
flavor: "您開始習慣了嗎?"
_login15:
title: "初學者III"
title: "初學者"
description: "總登入天數為15天"
_login30:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為30天"
_login60:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為60天"
_login100:
title: "Misskeyist "
title: "Misskist "
description: "總登入天數為100天"
flavor: "辣個 Misskeyist 用戶"
flavor: "辣個 Misskist 用戶"
_login200:
title: "普通Ⅰ"
description: "總登入天數為200天"
@@ -1089,7 +1089,7 @@ _achievements:
title: "請排成一排"
description: "跟隨者超過300人了"
_followers500:
title: "基"
title: "基地台"
description: "超過500名追隨者了"
_followers1000:
title: "影響者"
@@ -1111,7 +1111,7 @@ _achievements:
title: "休息一下"
description: "用戶端啟動已超過30分鐘"
_noteDeletedWithin1min:
title: "現在沒有"
title: "現在沒有"
description: "發文後1分鐘內刪文"
_postedAtLateNight:
title: "夜行性"
@@ -1181,7 +1181,7 @@ _role:
name: "角色名稱"
description: "角色描述 "
permission: "角色的權限"
descriptionOfPermission: "<b>審員</b>執行與審相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
descriptionOfPermission: "<b>審員</b>執行與審相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
assignTarget: "指派目標"
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
manual: "手動"
@@ -1195,8 +1195,11 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"
_priority:
low: "低"
@@ -1233,7 +1236,7 @@ _role:
or: "~或~"
not: "~否"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.2.2",
"version": "13.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -19,7 +19,7 @@
"start": "cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"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",
"gulp": "pnpm exec gulp build",
"watch": "pnpm dev",
@@ -28,8 +28,8 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress 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-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": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage",
"format": "pnpm exec gulp format",
@@ -38,8 +38,8 @@
"cleanall": "pnpm clean-all"
},
"resolutions": {
"chokidar": "^3.5.3",
"lodash": "^4.17.21"
"chokidar": "3.5.3",
"lodash": "4.17.21"
},
"dependencies": {
"execa": "5.1.1",
@@ -49,19 +49,19 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "4.9.4"
"typescript": "4.9.5"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"cross-env": "7.0.3",
"cypress": "12.3.0",
"eslint": "^8.32.0",
"cypress": "12.5.1",
"eslint": "8.33.0",
"start-server-and-test": "1.15.3"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "^4.2.0"
"@tensorflow/tfjs-core": "4.2.0"
}
}

View 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`);
}
}

View 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"`);
}
}

View File

@@ -1,6 +1,6 @@
import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js';
import { entities } from './built/postgre.js';
import { entities } from './built/postgres.js';
const config = loadConfig();

View File

@@ -19,27 +19,27 @@
"test-and-coverage": "pnpm jest-and-coverage"
},
"optionalDependencies": {
"@tensorflow/tfjs": "^4.2.0",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@bull-board/api": "^4.11.0",
"@bull-board/fastify": "^4.11.0",
"@bull-board/ui": "^4.11.0",
"@bull-board/api": "4.11.0",
"@bull-board/fastify": "4.11.0",
"@bull-board/ui": "4.11.0",
"@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0",
"@fastify/http-proxy": "^8.4.0",
"@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.0",
"@fastify/static": "6.6.1",
"@fastify/view": "7.4.0",
"@nestjs/common": "9.2.1",
"@nestjs/core": "9.2.1",
"@nestjs/testing": "9.2.1",
"@fastify/static": "6.8.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.1",
"@nestjs/core": "9.3.1",
"@nestjs/testing": "9.3.1",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"accepts": "^1.3.8",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
"autwh": "0.1.0",
@@ -62,11 +62,11 @@
"feed": "4.2.2",
"file-type": "18.2.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "^4.0.0",
"got": "^12.5.3",
"form-data": "4.0.0",
"got": "12.5.3",
"hpagent": "1.2.0",
"ioredis": "4.28.5",
"ip-cidr": "3.0.11",
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.0",
@@ -75,15 +75,16 @@
"jsrsasign": "10.6.1",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "0.0.14",
"misskey-js": "0.0.15",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"nodemailer": "6.9.0",
"node-fetch": "3.3.0",
"nodemailer": "6.9.1",
"nsfwjs": "2.4.2",
"oauth": "^0.10.0",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"parse5": "7.1.2",
"pg": "8.8.0",
"pg": "8.9.0",
"private-ip": "3.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@@ -101,23 +102,22 @@
"rss-parser": "3.12.0",
"rxjs": "7.8.0",
"s-age": "1.1.2",
"sanitize-html": "2.8.1",
"seedrandom": "^3.0.5",
"sanitize-html": "2.9.0",
"seedrandom": "3.0.5",
"semver": "7.3.8",
"sharp": "0.31.3",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "2.7.0",
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
"systeminformation": "5.17.3",
"systeminformation": "5.17.8",
"tinycolor2": "1.5.2",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typescript": "4.9.4",
"typescript": "4.9.5",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
@@ -125,29 +125,29 @@
"web-push": "3.5.0",
"websocket": "1.0.34",
"ws": "8.12.0",
"xev": "3.0.2",
"node-fetch": "3.3.0"
"xev": "3.0.2"
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.120",
"@swc/cli": "^0.1.59",
"@swc/core": "1.3.27",
"@jest/globals": "29.4.1",
"@redocly/openapi-core": "1.0.0-beta.123",
"@swc/cli": "0.1.61",
"@swc/core": "1.3.32",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0",
"@types/cbor": "6.0.0",
"@types/color-convert": "^2.0.0",
"@types/content-disposition": "^0.5.5",
"@types/color-convert": "2.0.0",
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20",
"@types/ioredis": "4.28.10",
"@types/jest": "29.2.6",
"@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.4",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
"@types/node": "18.11.18",
"@types/node-fetch": "3.0.3",
@@ -166,7 +166,6 @@
"@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/unzipper": "0.10.5",
@@ -175,13 +174,13 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"cross-env": "7.0.3",
"eslint": "8.32.0",
"eslint": "8.33.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.3.1",
"jest-mock": "^29.3.1"
"jest": "29.4.1",
"jest-mock": "29.4.1"
}
}

View File

@@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgreDataSource } from './postgre.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
@@ -18,7 +18,7 @@ const $config: Provider = {
const $db: Provider = {
provide: DI.db,
useFactory: async (config) => {
const db = createPostgreDataSource(config);
const db = createPostgresDataSource(config);
return await db.initialize();
},
inject: [DI.config],

View File

@@ -65,11 +65,6 @@ export type Source = {
deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number;
syslog: {
host: string;
port: number;
};
mediaProxy?: string;
proxyRemoteFiles?: boolean;
@@ -92,6 +87,8 @@ export type Mixin = {
userAgent: string;
clientEntry: string;
clientManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
};
export type Config = Source & Mixin;
@@ -113,7 +110,7 @@ const path = process.env.NODE_ENV === 'test'
export function loadConfig() {
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 ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
: { 'src/init.ts': { file: 'src/init.ts' } };
@@ -140,6 +137,13 @@ export function loadConfig() {
mixin.clientEntry = clientManifest['src/init.ts'];
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;
return Object.assign(config, mixin);

View File

@@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import { Cache } from '@/misc/cache.js';
import type { Packed } from '@/misc/schema.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 { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
export class AntennaService implements OnApplicationShutdown {
private antennasFetched: boolean;
private antennas: Antenna[];
private blockingCache: Cache<User['id'][]>;
constructor(
@Inject(DI.redisSubscriber)
@@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private noteEntityService: NoteEntityService,
private antennaEntityService: AntennaEntityService,
) {
this.antennasFetched = false;
this.antennas = [];
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
this.redisSubscriber.on('message', this.onRedisMessage);
}
@@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
read: read,
});
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
@@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
@@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') 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;

View File

@@ -23,9 +23,9 @@ export class CaptchaService {
const res = await this.httpRequestService.send(url, {
method: 'POST',
body: JSON.stringify(params),
body: params.toString(),
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
}, { throwErrorWhenResponseNotOk: false });

View File

@@ -62,7 +62,6 @@ import PerUserNotesChart from './chart/charts/per-user-notes.js';
import PerUserPvChart from './chart/charts/per-user-pv.js';
import DriveChart from './chart/charts/drive.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 PerUserDriveChart from './chart/charts/per-user-drive.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 $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart };
const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart };
const $HashtagChart: Provider = { provide: 'HashtagChart', useExisting: HashtagChart };
const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart };
const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart };
const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart };
@@ -315,7 +313,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart,
DriveChart,
PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart,
PerUserDriveChart,
ApRequestChart,
@@ -437,7 +434,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart,
$DriveChart,
$PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart,
$PerUserDriveChart,
$ApRequestChart,
@@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PerUserPvChart,
DriveChart,
PerUserReactionsChart,
HashtagChart,
PerUserFollowingChart,
PerUserDriveChart,
ApRequestChart,
@@ -680,7 +675,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PerUserPvChart,
$DriveChart,
$PerUserReactionsChart,
$HashtagChart,
$PerUserFollowingChart,
$PerUserDriveChart,
$ApRequestChart,

View File

@@ -26,7 +26,7 @@ export class CreateNotificationService {
private notificationEntityService: NotificationEntityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
) {
}
@@ -60,7 +60,7 @@ export class CreateNotificationService {
const packed = await this.notificationEntityService.pack(notification, {});
// Publish notification event
this.globalEventServie.publishMainStream(notifieeId, 'notification', packed);
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => {
@@ -77,7 +77,7 @@ export class CreateNotificationService {
}
//#endregion
this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed);
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));

View File

@@ -2,22 +2,39 @@ import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository } from '@/models/index.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
@Injectable()
export class CustomEmojiService {
private cache: Cache<Emoji | null>;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private utilityService: UtilityService,
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) {
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
}
@bindThis
@@ -40,8 +57,127 @@ export class CustomEmojiService {
type: data.driveFile.webpublicType ?? data.driveFile.type,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
await this.db.queryResultCache!.remove(['meta_emojis']);
if (data.host == null) {
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.pack(emoji.id),
});
}
return emoji;
}
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
: this.utilityService.isSelfHost(src) ? null // 自ホスト指定
: (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
host = this.utilityService.toPunyNullable(host);
return host;
}
@bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
const name = match[1];
// ホスト正規化
const host = this.utilityService.toPunyNullable(this.normalizeHost(match[2], noteUserHost));
return { name, host };
}
/**
* 添付用(リモート)カスタム絵文字URLを解決する
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
* @returns URL, nullは未マッチを意味する
*/
@bindThis
public async populateEmoji(emojiName: string, noteUserHost: string | null): Promise<string | null> {
const { name, host } = this.parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null;
if (host == null) return null;
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
name,
host: host ?? IsNull(),
})) ?? null;
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
const url = isLocal
? emojiUrl
: this.config.proxyRemoteFiles
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl;
return url;
}
/**
* 複数の添付用(リモート)カスタム絵文字URLを解決する (キャシュ付き, 存在しないものは結果から除外される)
*/
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
const res = {} as any;
for (let i = 0; i < emojiNames.length; i++) {
if (emojis[i] != null) {
res[emojiNames[i]] = emojis[i];
}
}
return res;
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/**
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
*/
@bindThis
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
const emojisQuery: any[] = [];
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
if (host == null) continue;
emojisQuery.push({
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
host: host,
});
}
const _emojis = emojisQuery.length > 0 ? await this.emojisRepository.find({
where: emojisQuery,
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}) : [];
for (const emoji of _emojis) {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}
}

View File

@@ -14,7 +14,7 @@ export class DeleteAccountService {
private userSuspendService: UserSuspendService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
) {
}
@@ -38,6 +38,6 @@ export class DeleteAccountService {
});
// Terminate streaming
this.globalEventServie.publishUserEvent(user.id, 'terminate', {});
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
}

View File

@@ -60,6 +60,7 @@ export class DownloadService {
retry: {
limit: 0,
},
enableUnixSockets: false,
}).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) {

View File

@@ -4,7 +4,6 @@ import type { User } from '@/models/entities/User.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { IdService } from '@/core/IdService.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
@@ -20,7 +19,6 @@ export class HashtagService {
private userEntityService: UserEntityService,
private idService: IdService,
private hashtagChart: HashtagChart,
) {
}
@@ -143,9 +141,5 @@ export class HashtagService {
} as Hashtag);
}
}
if (!isUserAttached) {
this.hashtagChart.update(tag, user);
}
}
}

View File

@@ -95,7 +95,7 @@ export class HttpRequestService {
}
@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, {
method: 'GET',
headers: Object.assign({
@@ -106,7 +106,7 @@ export class HttpRequestService {
size: 1024 * 256,
});
return await res.json();
return await res.json() as T;
}
@bindThis

View File

@@ -9,6 +9,14 @@ export type IImage = {
type: string;
};
export type IImageStream = {
data: Readable;
ext: string | null;
type: string;
};
export type IImageStreamable = IImage | IImageStream;
export const webpDefault: sharp.WebpOptions = {
quality: 85,
alphaQuality: 95,
@@ -19,6 +27,7 @@ export const webpDefault: sharp.WebpOptions = {
};
import { bindThis } from '@/decorators.js';
import { Readable } from 'node:stream';
@Injectable()
export class ImageProcessingService {
@@ -64,7 +73,7 @@ export class ImageProcessingService {
*/
@bindThis
public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
return this.convertSharpToWebp(await sharp(path), width, height, options);
return this.convertSharpToWebp(sharp(path), width, height, options);
}
@bindThis
@@ -85,6 +94,27 @@ export class ImageProcessingService {
};
}
@bindThis
public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
return this.convertSharpToWebpStream(sharp(path), width, height, options);
}
@bindThis
public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
const data = sharp
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true,
})
.rotate()
.webp(options)
return {
data,
ext: 'webp',
type: 'image/webp',
};
}
/**
* Convert to PNG
* with resize, remove metadata, resolve orientation, stop animation

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import * as SyslogPro from 'syslog-pro';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import Logger from '@/logger.js';
@@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions';
@Injectable()
export class LoggerService {
private syslogClient;
constructor(
@Inject(DI.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
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain, color, store, this.syslogClient);
return new Logger(domain, color, store);
}
}

View File

@@ -175,7 +175,7 @@ export class NoteCreateService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
private noteReadService: NoteReadService,
private createNotificationService: CreateNotificationService,
@@ -535,7 +535,7 @@ export class NoteCreateService {
// Pack the note
const noteObj = await this.noteEntityService.pack(note);
this.globalEventServie.publishNotesStream(noteObj);
this.globalEventService.publishNotesStream(noteObj);
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
@@ -561,7 +561,7 @@ export class NoteCreateService {
if (!threadMuted) {
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'));
for (const webhook of webhooks) {
@@ -584,7 +584,7 @@ export class NoteCreateService {
// Publish event
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'));
for (const webhook of webhooks) {
@@ -684,7 +684,7 @@ export class NoteCreateService {
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'));
for (const webhook of webhooks) {

View File

@@ -34,7 +34,7 @@ export class NoteDeleteService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private relayService: RelayService,
private federatedInstanceService: FederatedInstanceService,
private apRendererService: ApRendererService,
@@ -63,7 +63,7 @@ export class NoteDeleteService {
}
if (!quiet) {
this.globalEventServie.publishNoteStream(note.id, 'deleted', {
this.globalEventService.publishNoteStream(note.id, 'deleted', {
deletedAt: deletedAt,
});

View File

@@ -9,9 +9,9 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { bindThis } from '@/decorators.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable()
@@ -40,7 +40,7 @@ export class NoteReadService {
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
@@ -87,13 +87,13 @@ export class NoteReadService {
if (exist == null) return;
if (params.isMentioned) {
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id);
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
}
if (params.isSpecified) {
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
if (note.channelId) {
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id);
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, 2000);
}
@@ -107,12 +107,6 @@ export class NoteReadService {
followingChannels: Set<Channel['id']>;
},
): 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({
where: {
followerId: userId,
@@ -139,7 +133,7 @@ export class NoteReadService {
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
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);
}
}
@@ -161,7 +155,7 @@ export class NoteReadService {
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions');
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
});
@@ -171,7 +165,7 @@ export class NoteReadService {
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
@@ -181,7 +175,7 @@ export class NoteReadService {
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllChannels');
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
@@ -206,14 +200,14 @@ export class NoteReadService {
});
if (count === 0) {
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna);
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventServie.publishMainStream(userId, 'readAllAntennas');
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});

View File

@@ -1,17 +1,17 @@
import { Inject, Injectable } from '@nestjs/common';
import { Not } from 'typeorm';
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 { RelayService } from '@/core/RelayService.js';
import type { CacheableUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
@Injectable()
export class PollService {
@@ -28,14 +28,11 @@ export class PollService {
@Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private relayService: RelayService,
private globalEventServie: GlobalEventService,
private createNotificationService: CreateNotificationService,
private globalEventService: GlobalEventService,
private userBlockingService: UserBlockingService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
) {
@@ -52,11 +49,8 @@ export class PollService {
// Check blocking
if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: note.userId,
blockeeId: user.id,
});
if (block) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
if (blocked) {
throw new Error('blocked');
}
}
@@ -88,7 +82,7 @@ export class PollService {
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}'`);
this.globalEventServie.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
choice: choice,
userId: user.id,
});

View File

@@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { MetaService } from '@/core/MetaService.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> = {
'like': '👍',
@@ -73,8 +74,9 @@ export class ReactionService {
private metaService: MetaService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
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) {
// Check blocking
if (note.userId !== user.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: note.userId,
blockeeId: user.id,
});
if (block) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
if (blocked) {
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
}
}
@@ -157,7 +156,7 @@ export class ReactionService {
select: ['name', 'host', 'originalUrl', 'publicUrl'],
});
this.globalEventServie.publishNoteStream(note.id, 'reacted', {
this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction,
emoji: emoji != null ? {
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);
this.globalEventServie.publishNoteStream(note.id, 'unreacted', {
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id,
});

View File

@@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
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
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch();

View File

@@ -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 type { CacheableUser, User } from '@/models/entities/User.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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.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 Logger from '@/logger.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 { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { Cache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@Injectable()
export class UserBlockingService {
export class UserBlockingService implements OnApplicationShutdown {
private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: Cache<User['id'][]>;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -42,13 +50,44 @@ export class UserBlockingService {
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService,
) {
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
@@ -72,6 +111,11 @@ export class UserBlockingService {
await this.blockingsRepository.insert(blocking);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
this.queueService.deliver(blocker, content, blockee.inbox);
@@ -97,15 +141,15 @@ export class UserBlockingService {
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, {
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)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {
@@ -152,8 +196,8 @@ export class UserBlockingService {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {
@@ -210,10 +254,31 @@ export class UserBlockingService {
await this.blockingsRepository.delete(blocking.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
});
// deliver if remote bloking
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
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);
}
}

View File

@@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.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 { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@@ -48,21 +49,18 @@ export class UserFollowingService {
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@@ -77,28 +75,22 @@ export class UserFollowingService {
// check blocking
const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({
blockerId: follower.id,
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox);
return;
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.blockingsRepository.delete(blocking.id);
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
await this.userBlockingService.unblock(follower, followee);
} else {
// それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
// それ以外は単純に例外
if (blocking) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
}
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
@@ -227,8 +219,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {
@@ -242,7 +234,7 @@ export class UserFollowingService {
// Publish followed event
if (this.userEntityService.isLocalUser(followee)) {
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'));
for (const webhook of webhooks) {
@@ -288,8 +280,8 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {
@@ -357,18 +349,12 @@ export class UserFollowingService {
// check blocking
const [blocking, blocked] = await Promise.all([
this.blockingsRepository.findOneBy({
blockerId: follower.id,
blockeeId: followee.id,
}),
this.blockingsRepository.findOneBy({
blockerId: followee.id,
blockeeId: follower.id,
}),
this.userBlockingService.checkBlocked(follower.id, followee.id),
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (blocking != null) throw new Error('blocking');
if (blocked != null) throw new Error('blocked');
if (blocking) throw new Error('blocking');
if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(),
@@ -388,11 +374,11 @@ export class UserFollowingService {
// Publish receiveRequest event
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, {
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', {
@@ -440,7 +426,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@bindThis
@@ -468,7 +454,7 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, followee, {
detail: true,
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@bindThis
@@ -583,8 +569,8 @@ export class UserFollowingService {
detail: true,
});
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishUserEvent(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'));
for (const webhook of webhooks) {

View File

@@ -25,7 +25,7 @@ export class UserListService {
private idService: IdService,
private userFollowingService: UserFollowingService,
private roleService: RoleService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
) {
}
@@ -46,7 +46,7 @@ export class UserListService {
userListId: list.id,
} 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)) {

View File

@@ -18,7 +18,7 @@ export class UserMutingService {
private idService: IdService,
private queueService: QueueService,
private globalEventServie: GlobalEventService,
private globalEventService: GlobalEventService,
) {
}

View File

@@ -44,16 +44,25 @@ export class WebhookService implements OnApplicationShutdown {
switch (type) {
case 'webhookCreated':
if (body.active) {
this.webhooks.push(body);
this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
}
break;
case 'webhookUpdated':
if (body.active) {
const i = this.webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
this.webhooks[i] = body;
this.webhooks[i] = {
...body,
createdAt: new Date(body.createdAt),
};
} else {
this.webhooks.push(body);
this.webhooks.push({
...body,
createdAt: new Date(body.createdAt),
});
}
} else {
this.webhooks = this.webhooks.filter(a => a.id !== body.id);

View File

@@ -274,7 +274,7 @@ export class ApRendererService {
} as any;
if (reaction.startsWith(':')) {
const name = reaction.replace(/:/g, '');
const name = reaction.replaceAll(':', '');
const emoji = await this.emojisRepository.findOneBy({
name,
host: IsNull(),

View File

@@ -1,5 +1,6 @@
import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import jsonld from 'jsonld';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js';
@@ -84,7 +85,9 @@ class LdSignature {
@bindThis
public async normalize(data: any) {
const customLoader = this.getLoader();
return 42;
return await jsonld.normalize(data, {
documentLoader: customLoader,
});
}
@bindThis

View File

@@ -48,6 +48,10 @@ export class ApImageService {
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}`);
const instance = await this.metaService.fetch();

View File

@@ -1,8 +1,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js';
import type { UsersRepository } from '@/models/index.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { CacheableRemoteUser } from '@/models/entities/User.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 { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
@@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApNoteService {
@@ -133,6 +132,16 @@ export class ApNoteService {
const note: IPost = object;
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}`);
@@ -307,7 +316,7 @@ export class ApNoteService {
apEmojis,
poll,
uri: note.id,
url: getOneApHrefNullable(note.url),
url: url,
}, silent);
}

View File

@@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
import { StatusError } from '@/misc/status-error.js';
import type { UtilityService } from '@/core/UtilityService.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 { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
const nameLength = 128;
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()
export class ApPersonService implements OnModuleInit {
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 url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
// Create user
let user: IRemoteUser;
try {
@@ -313,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: getOneApHrefNullable(person.url),
url: url,
fields,
birthday: bday ? bday[0] : 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 url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
@@ -489,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
}
await this.userProfilesRepository.update({ userId: exist.id }, {
url: getOneApHrefNullable(person.url),
url: url,
fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday ? bday[0] : null,
@@ -540,22 +522,16 @@ export class ApPersonService implements OnModuleInit {
name: string,
value: string
}[] = [];
const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({
name: attachment.name,
value: this.mfmService.fromHtml(attachment.value),
});
}
fields.push({
name: attachment.name,
value: this.mfmService.fromHtml(attachment.value),
});
}
}
return { fields, services };
return { fields };
}
@bindThis

View File

@@ -10,7 +10,6 @@ import PerUserNotesChart from './charts/per-user-notes.js';
import PerUserPvChart from './charts/per-user-pv.js';
import DriveChart from './charts/drive.js';
import PerUserReactionsChart from './charts/per-user-reactions.js';
import HashtagChart from './charts/hashtag.js';
import PerUserFollowingChart from './charts/per-user-following.js';
import PerUserDriveChart from './charts/per-user-drive.js';
import ApRequestChart from './charts/ap-request.js';
@@ -31,7 +30,6 @@ export class ChartManagementService implements OnApplicationShutdown {
private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart,
@@ -46,7 +44,6 @@ export class ChartManagementService implements OnApplicationShutdown {
this.perUserPvChart,
this.driveChart,
this.perUserReactionsChart,
this.hashtagChart,
this.perUserFollowingChart,
this.perUserDriveChart,
this.apRequestChart,

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -11,9 +11,9 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { Repository, DataSource } from 'typeorm';
const columnPrefix = '___' as const;
const uniqueTempColumnPrefix = 'unique_temp___' as const;
const columnDot = '_' as const;
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
const COLUMN_DELIMITER = '_' as const;
type Schema = Record<string, {
uniqueIncrement?: boolean;
@@ -26,14 +26,14 @@ type Schema = Record<string, {
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> = {
[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> = {
[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> = {
@@ -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; }> {
const columns = {} as Record<string, { type: string; array?: boolean; default?: any; }>;
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';
if (v.uniqueIncrement) {
columns[uniqueTempColumnPrefix + name] = {
columns[UNIQUE_TEMP_COLUMN_PREFIX + name] = {
type: 'varchar',
array: true,
default: '{}',
};
columns[columnPrefix + name] = {
columns[COLUMN_PREFIX + name] = {
type,
default: 0,
};
} else {
columns[columnPrefix + name] = {
columns[COLUMN_PREFIX + name] = {
type,
default: 0,
};
@@ -253,8 +253,8 @@ export default abstract class Chart<T extends Schema> {
@bindThis
private convertRawRecord(x: RawRecord<T>): KVs<T> {
const kvs = {} as Record<string, number>;
for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k] as unknown as number;
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
}
return kvs as KVs<T>;
}
@@ -357,8 +357,8 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<string, number | unknown[]>;
for (const [k, v] of Object.entries(data)) {
const name = k.replaceAll('.', columnDot);
columns[columnPrefix + name] = v;
const name = k.replaceAll('.', COLUMN_DELIMITER);
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;
for (const [k, v] of Object.entries(finalDiffs)) {
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}" - ${Math.abs(v)}`;
if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`;
if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`;
} 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エスケープ
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}"`);
@@ -437,8 +437,8 @@ export default abstract class Chart<T extends Schema> {
// bake unique count
for (const [k, v] of Object.entries(finalDiffs)) {
if (this.schema[k].uniqueIncrement) {
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>;
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<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;
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)) {
const intersection = v.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 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 currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]);
const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]);
for (let i = 1; i < intersection.length; 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 targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[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>;
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;
}
@@ -556,7 +556,7 @@ export default abstract class Chart<T extends Schema> {
const columns = {} as Record<keyof TempColumnsForUnique<T>, []>;
for (const [k, v] of Object.entries(this.schema)) {
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] = [];
}
}

View File

@@ -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 DriveChart } from './charts/entities/drive.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 PerUserDriveChart } from './charts/entities/per-user-drive.js';
import { entity as ApRequestChart } from './charts/entities/ap-request.js';
@@ -27,7 +26,6 @@ export const entities = [
PerUserPvChart.hour, PerUserPvChart.day,
DriveChart.hour, DriveChart.day,
PerUserReactionsChart.hour, PerUserReactionsChart.day,
HashtagChart.hour, HashtagChart.day,
PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day,
ApRequestChart.hour, ApRequestChart.day,

View File

@@ -54,7 +54,7 @@ export class ChannelEntityService {
name: channel.name,
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
usersCount: channel.usersCount,
notesCount: channel.notesCount,

View File

@@ -20,6 +20,7 @@ type PackOptions = {
withUser?: boolean,
};
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
@Injectable()
export class DriveFileEntityService {
@@ -71,27 +72,42 @@ export class DriveFileEntityService {
}
@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) {
return appendQuery(this.config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? '1' : undefined,
}));
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
if (!(mode === 'static' && file.type.startsWith('video'))) {
return proxiedUrl(file.uri);
}
}
// リモートかつ期限切れはローカルプロキシを試みる
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('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
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
@@ -166,8 +182,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
@@ -201,8 +217,8 @@ export class DriveFileEntityService {
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
url: opts.self ? file.url : this.getPublicUrl(file),
thumbnailUrl: this.getPublicUrl(file, 'static'),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {

View File

@@ -22,8 +22,10 @@ export class EmojiEntityService {
@bindThis
public async pack(
src: Emoji['id'] | Emoji,
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = { omitHost: true, omitId: true, withUrl: true },
): Promise<Packed<'Emoji'>> {
opts = { omitHost: true, omitId: true, withUrl: true, ...opts }
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
return {

View File

@@ -282,7 +282,9 @@ export class NoteEntityService implements OnModuleInit {
: await this.channelsRepository.findOneBy({ id: note.channelId })
: null;
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x.startsWith(':')).map(x => this.reactionService.decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
const reactionEmojiNames = Object.keys(note.reactions)
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
@@ -299,6 +301,8 @@ export class NoteEntityService implements OnModuleInit {
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds,
files: this.driveFileEntityService.packMany(note.fileIds),
@@ -384,6 +388,8 @@ export class NoteEntityService implements OnModuleInit {
}
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
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 type { Notification } from '@/models/entities/Notification.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
@@ -146,6 +146,8 @@ export class NotificationEntityService implements OnModuleInit {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, {
_hintForEachNotes_: {
myReactions: myReactionsMap,

View File

@@ -56,11 +56,13 @@ export class RoleEntityService {
name: role.name,
description: role.description,
color: role.color,
iconUrl: role.iconUrl,
target: role.target,
condFormula: role.condFormula,
isPublic: role.isPublic,
isAdministrator: role.isAdministrator,
isModerator: role.isModerator,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies,
usersCount: assigns.length,

View File

@@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getAvatarUrl(user: User): Promise<string> {
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) {
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 {
return this.getIdenticonUrl(user.id);
}
@@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public getAvatarUrlSync(user: User): string {
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 {
return this.getIdenticonUrl(user.id);
}
@@ -414,6 +414,11 @@ export class UserEntityService implements OnModuleInit {
themeColor: instance.themeColor,
} : undefined) : undefined,
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 ? {
url: profile!.url,
@@ -421,7 +426,7 @@ export class UserEntityService implements OnModuleInit {
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.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,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
@@ -453,10 +458,12 @@ export class UserEntityService implements OnModuleInit {
id: role.id,
name: role.name,
color: role.color,
iconUrl: role.iconUrl,
description: role.description,
isModerator: role.isModerator,
isAdministrator: role.isAdministrator,
}))),
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
} : {}),
...(opts.detail && isMe ? {
@@ -488,7 +495,6 @@ export class UserEntityService implements OnModuleInit {
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
@@ -496,10 +502,10 @@ export class UserEntityService implements OnModuleInit {
showTimelineReplies: user.showTimelineReplies ?? falsy,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),
} : {}),
...(opts.includeSecrets ? {
policies: this.roleService.getUserPolicies(user.id),
email: profile!.email,
emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled

View File

@@ -17,15 +17,13 @@ export default class Logger {
private context: Context;
private parentLogger: Logger | null = null;
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 = {
name: context,
color: color,
};
this.store = store;
this.syslogClient = syslogClient;
}
@bindThis
@@ -68,20 +66,7 @@ export default class Logger {
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log);
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(() => {});
}
}
if (level === 'error' && data) console.log(data);
}
@bindThis

View File

@@ -1,5 +1,7 @@
import { bindThis } from '@/decorators.js';
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number;

View File

@@ -4,7 +4,7 @@ import { unique } from '@/misc/prelude/array.js';
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
const emojiNodes = mfm.extract(nodes, (node) => {
return (node.type === 'emojiCode' && node.props.name.length <= 100);
});
}) as mfm.MfmEmojiCode[];
return unique(emojiNodes.map(x => x.props.name));
}

View File

@@ -2,7 +2,7 @@ import * as mfm from 'mfm-js';
import { unique } from '@/misc/prelude/array.js';
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));
return hashtags;

View File

@@ -1,14 +1,14 @@
export function nyaize(text: string): string {
return text
// ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');

View File

@@ -132,11 +132,27 @@ type NullOrUndefined<p extends Schema, T> =
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union
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
// 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 ArrayUnion<T> = T extends any ? Array<T> : never;
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
p['properties'] extends NonNullable<Obj> ?
p['anyOf'] extends ReadonlyArray<Schema> ?
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
:
ObjType<p['properties'], NonNullable<p['required']>[number]>
:
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
any
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'null' ? null :
@@ -149,13 +165,7 @@ export type SchemaTypeDef<p extends Schema> =
string
) :
p['type'] extends 'boolean' ? boolean :
p['type'] extends 'object' ? (
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 'object' ? ObjectSchemaTypeDef<p> :
p['type'] extends 'array' ? (
p['items'] extends OfSchema ? (
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']>[] :
any[]
) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
any;

View File

@@ -279,57 +279,6 @@ export class Meta {
})
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', {
length: 128,
nullable: true,

View File

@@ -102,6 +102,11 @@ export class Role {
})
public color: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public iconUrl: string | null;
@Column('enum', {
enum: ['manual', 'conditional'],
default: 'manual',
@@ -118,6 +123,12 @@ export class Role {
})
public isPublic: boolean;
// trueの場合ユーザー名の横にバッジとして表示
@Column('boolean', {
default: false,
})
public asBadge: boolean;
@Column('boolean', {
default: false,
})

View File

@@ -184,11 +184,6 @@ export class UserProfile {
@JoinColumn()
public pinnedPage: Page | null;
@Column('jsonb', {
default: {},
})
public integrations: Record<string, any>;
@Index()
@Column('boolean', {
default: false, select: false,

View File

@@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
integrations: {
type: 'object',
nullable: true, optional: false,
},
mutedWords: {
type: 'array',
nullable: false, optional: false,

View File

@@ -197,7 +197,7 @@ export const entities = [
const log = process.env.NODE_ENV !== 'production';
export function createPostgreDataSource(config: Config) {
export function createPostgresDataSource(config: Config) {
return new DataSource({
type: 'postgres',
host: config.db.host,

View File

@@ -57,8 +57,15 @@ export class AggregateRetentionProcessorService {
usersCount: targetUserIds.length,
});
// 今日活動したユーザーを全て取得
const activeUsers = await this.usersRepository.findBy({
host: IsNull(),
lastActiveDate: MoreThan(new Date(Date.now() - (1000 * 60 * 60 * 24))),
});
const activeUsersIds = activeUsers.map(u => u.id);
for (const record of pastRecords) {
const retention = record.userIds.filter(id => targetUserIds.includes(id)).length;
const retention = record.userIds.filter(id => activeUsersIds.includes(id)).length;
const data = deepClone(record.data);
data[dateKey] = retention;

View File

@@ -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 DriveChart from '@/core/chart/charts/drive.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 PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
@@ -37,7 +36,6 @@ export class CleanChartsProcessorService {
private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart,
@@ -61,7 +59,6 @@ export class CleanChartsProcessorService {
this.perUserPvChart.clean(),
this.driveChart.clean(),
this.perUserReactionsChart.clean(),
this.hashtagChart.clean(),
this.perUserFollowingChart.clean(),
this.perUserDriveChart.clean(),
this.apRequestChart.clean(),

View File

@@ -12,9 +12,9 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportCustomEmojisProcessorService {
@@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService {
});
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 fileName = emoji.name + (ext ? '.' + ext : '');
const emojiPath = path + '/' + fileName;

View File

@@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService {
for (const record of meta.emojis) {
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 emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({

View File

@@ -11,13 +11,12 @@ import InstanceChart from '@/core/chart/charts/instance.js';
import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
import DriveChart from '@/core/chart/charts/drive.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 PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ResyncChartsProcessorService {
@@ -35,7 +34,6 @@ export class ResyncChartsProcessorService {
private perUserNotesChart: PerUserNotesChart,
private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart,

View File

@@ -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 DriveChart from '@/core/chart/charts/drive.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 PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
@@ -37,7 +36,6 @@ export class TickChartsProcessorService {
private perUserPvChart: PerUserPvChart,
private driveChart: DriveChart,
private perUserReactionsChart: PerUserReactionsChart,
private hashtagChart: HashtagChart,
private perUserFollowingChart: PerUserFollowingChart,
private perUserDriveChart: PerUserDriveChart,
private apRequestChart: ApRequestChart,
@@ -61,7 +59,6 @@ export class TickChartsProcessorService {
this.perUserPvChart.tick(false),
this.driveChart.tick(false),
this.perUserReactionsChart.tick(false),
this.hashtagChart.tick(false),
this.perUserFollowingChart.tick(false),
this.perUserDriveChart.tick(false),
this.apRequestChart.tick(false),

View File

@@ -5,14 +5,14 @@ import { Inject, Injectable } from '@nestjs/common';
import fastifyStatic from '@fastify/static';
import rename from 'rename';
import type { Config } from '@/config.js';
import type { DriveFilesRepository } from '@/models/index.js';
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js';
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js';
import { contentDisposition } from '@/misc/content-disposition.js';
@@ -20,6 +20,8 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import { isMimeImage } from '@/misc/is-mime-image.js';
import sharp from 'sharp';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -57,7 +59,7 @@ export class FileServerService {
reply.header('Cache-Control', 'max-age=300');
};
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
@@ -70,23 +72,329 @@ export class FileServerService {
serve: false,
});
fastify.get('/app-default.jpg', (request, reply) => {
fastify.get('/files/app-default.jpg', (request, reply) => {
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file);
});
fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply));
fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(request, reply));
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
return await this.sendDriveFile(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
return await this.sendDriveFile(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{
Params: { url: string; };
Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => {
return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err));
});
done();
}
@bindThis
private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) {
this.logger.error(`${err}`);
reply.header('Cache-Control', 'max-age=300');
if (request.query && 'fallback' in request.query) {
return reply.sendFile('/dummy.png', assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
reply.code(err.statusCode);
return;
}
reply.code(500);
return;
}
@bindThis
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
const key = request.params.key;
const file = await this.getFileFromKey(key).then();
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
if (file.state === 'remote') {
let image: IImageStreamable | null = null;
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
}
}
if (!image) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
return image.data;
}
if (file.fileRole !== 'original') {
const filename = rename(file.file.name, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : undefined,
}).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
return fs.createReadStream(file.path);
} else {
const stream = fs.createReadStream(file.path);
stream.on('error', this.commonReadableHandlerGenerator(reply));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.file.name));
return stream;
}
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
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
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
let image: IImageStreamable | null = null;
if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} else {
const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({
height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
} else if ('badge' in request.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(file.path)
.resize(96, 96, {
fit: 'inside',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (file.mime === 'image/svg+xml') {
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
if ('cleanup' in file) {
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
if (url.startsWith(`${this.config.url}/files/`)) {
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
return await this.getFileFromKey(key);
}
return await this.downloadAndDetectTypeFromUrl(url);
}
@bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
> {
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'remote',
mime, ext,
path, cleanup,
}
} catch (e) {
cleanup();
throw e;
}
}
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; 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; }
| '404'
| '204'
> {
// Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
@@ -94,89 +402,42 @@ export class FileServerService {
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
if (file == null) {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file == null) return '404';
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) {
if (file.isLink && file.uri) { // 期限切れリモートファイル
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(file.uri, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
const convertFile = async () => {
if (isThumbnail) {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(mime)) {
return await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if (mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(path);
}
}
if (isWebpublic) {
if (['image/svg+xml'].includes(mime)) {
return await this.imageProcessingService.convertToPng(path, 2048, 2048);
}
}
return {
data: fs.readFileSync(path),
ext,
type: mime,
};
};
const image = await convertFile();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (err) {
this.logger.error(`${err}`);
if (err instanceof StatusError && err.isClientError) {
reply.code(err.statusCode);
reply.header('Cache-Control', 'max-age=86400');
} else {
reply.code(500);
reply.header('Cache-Control', 'max-age=300');
}
} finally {
cleanup();
}
return;
if (!(file.isLink && file.uri)) return '204';
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return {
...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
}
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
if (isThumbnail || isWebpublic) {
const { mime, ext } = await this.fileInfoService.detectType(this.internalStorageService.resolvePath(key));
const filename = rename(file.name, {
suffix: isThumbnail ? '-thumb' : '-web',
extname: ext ? `.${ext}` : undefined,
}).toString();
const path = this.internalStorageService.resolvePath(key);
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
return this.internalStorageService.read(key);
} else {
const readable = this.internalStorageService.read(file.accessKey!);
readable.on('error', this.commonReadableHandlerGenerator(reply));
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.name));
return readable;
if (isThumbnail || isWebpublic) {
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'stored_internal',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file,
mime, ext,
path,
};
}
return {
state: 'stored_internal',
fileRole: 'original',
file,
mime: file.type,
ext: null,
path,
}
}
}

View File

@@ -1,177 +0,0 @@
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import sharp from 'sharp';
import fastifyStatic from '@fastify/static';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { createTemp } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import type { IImage } from '@/core/ImageProcessingService.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../src/server/assets/`;
@Injectable()
export class MediaProxyServerService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
private fileInfoService: FileInfoService,
private downloadService: DownloadService,
private imageProcessingService: ImageProcessingService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray', false);
//this.createServer = this.createServer.bind(this);
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
done();
});
fastify.register(fastifyStatic, {
root: _dirname,
serve: false,
});
fastify.get<{
Params: { url: string; };
Querystring: { url?: string; };
}>('/:url*', async (request, reply) => await this.handler(request, reply));
done();
}
@bindThis
private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
try {
await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
let image: IImage;
if ('emoji' in request.query && isConvertibleImage) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.readFileSync(path),
ext,
type: mime,
};
} else {
const data = await sharp(path, { animated: !('static' in request.query) })
.resize({
height: 128,
withoutEnlargement: true,
})
.webp(webpDefault)
.toBuffer();
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {
image = await this.imageProcessingService.convertToWebp(path, 200, 200);
} else if ('badge' in request.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(path)
.resize(96, 96, {
fit: 'inside',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (mime === 'image/svg+xml') {
image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault);
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
} else {
image = {
data: fs.readFileSync(path),
ext,
type: mime,
};
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
return image.data;
} catch (err) {
this.logger.error(`${err}`);
if ('fallback' in request.query) {
return reply.sendFile('/dummy.png', assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
reply.code(err.statusCode);
} else {
reply.code(500);
}
} finally {
cleanup();
}
}
}

View File

@@ -111,9 +111,6 @@ export class NodeinfoServerService {
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,

View File

@@ -3,14 +3,10 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
import { MediaProxyServerService } from './MediaProxyServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.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 { ActivityPubServerService } from './ActivityPubServerService.js';
import { ApiLoggerService } from './api/ApiLoggerService.js';
@@ -51,14 +47,10 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
UrlPreviewService,
ActivityPubServerService,
FileServerService,
MediaProxyServerService,
NodeinfoServerService,
ServerService,
WellKnownServerService,
GetterService,
DiscordServerService,
GithubServerService,
TwitterServerService,
ChannelsService,
ApiCallService,
ApiLoggerService,

View File

@@ -20,7 +20,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { MediaProxyServerService } from './MediaProxyServerService.js';
import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
@@ -48,7 +47,6 @@ export class ServerService {
private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService,
private mediaProxyServerService: MediaProxyServerService,
private clientServerService: ClientServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
@@ -73,8 +71,7 @@ export class ServerService {
}
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer, { prefix: '/files' });
fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer);
@@ -109,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なので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1');

View File

@@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.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';
@Injectable()
@@ -38,9 +35,6 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
private githubServerService: GithubServerService,
private discordServerService: DiscordServerService,
private twitterServerService: TwitterServerService,
) {
//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.register(this.discordServerService.create);
fastify.register(this.githubServerService.create);
fastify.register(this.twitterServerService.create);
fastify.get('/v1/instance/peers', async (request, reply) => {
const instances = await this.instancesRepository.find({
select: ['host'],

View File

@@ -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_drive from './endpoints/charts/drive.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_notes from './endpoints/charts/notes.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_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_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_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 };
@@ -773,7 +771,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest,
$charts_drive,
$charts_federation,
$charts_hashtag,
$charts_instance,
$charts_notes,
$charts_user_drive,
@@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$charts_apRequest,
$charts_drive,
$charts_federation,
$charts_hashtag,
$charts_instance,
$charts_notes,
$charts_user_drive,

View File

@@ -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_drive from './endpoints/charts/drive.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_notes from './endpoints/charts/notes.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/drive', ep___charts_drive],
['charts/federation', ep___charts_federation],
['charts/hashtag', ep___charts_hashtag],
['charts/instance', ep___charts_instance],
['charts/notes', ep___charts_notes],
['charts/user/drive', ep___charts_user_drive],

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