Compare commits

..

123 Commits

Author SHA1 Message Date
syuilo
000f876084 Merge branch 'develop' 2023-02-10 20:14:47 +09:00
syuilo
2d11c558fa 13.5.6 2023-02-10 20:14:38 +09:00
syuilo
ac6b02af40 New Crowdin updates (#9852)
* 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)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)
2023-02-10 20:13:07 +09:00
Nya Candy
7d91912cfd fix: prevent clipping audio plyr's tooltip (#9850) 2023-02-10 18:29:54 +09:00
syuilo
3c504b4b08 chore(client): improve usability 2023-02-10 11:04:11 +09:00
syuilo
adad4bcfe3 クロップ時の質問を分かりやすく 2023-02-10 10:45:32 +09:00
syuilo
b3e8671dd9 利用規約同意UIの調整 2023-02-10 10:36:25 +09:00
syuilo
0f8c890761 🎨 2023-02-10 09:49:52 +09:00
tamaina
512e451f24 app auth / miauthの文言編集 2023-02-09 17:29:22 +00:00
tamaina
ca0d53ec5d enhance(client): /authおよびMiAuthのUIをブラッシュアップ
Fix #9742
2023-02-09 17:18:27 +00:00
Acid Chicken (硫酸鶏)
686a709e87 chore: determine dimensions of the helix of cat ears based on the size of avatars (#9836)
* chore: determine dimensions of the helix of cat ears based on the size of avatars

* Update MkAvatar.vue

* Update packages/frontend/src/components/global/MkAvatar.vue

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-02-10 00:36:05 +09:00
tamaina
83fb629f0b 🎨 2023-02-09 15:34:49 +00:00
tamaina
35eeeb25e3 update pnpm to 7.27.0 2023-02-09 15:05:23 +00:00
tamaina
19035c676c /proxyでemoji, avatarなどの命令がありかつ画像でないなら404を返すように 2023-02-09 12:39:24 +00:00
syuilo
61ffe7417c Merge branch 'develop' 2023-02-09 20:48:07 +09:00
syuilo
7651353f39 13.5.5 2023-02-09 20:47:56 +09:00
syuilo
3f5b81060f New Crowdin updates (#9844)
* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Lao)
2023-02-09 20:47:33 +09:00
syuilo
63dc66769f fix(client): webkitでMkMediaListが崩れるのを修正 2023-02-09 20:12:36 +09:00
syuilo
e0fc8cbf8f Merge branch 'develop' 2023-02-09 18:12:04 +09:00
syuilo
f9d1bc340e 13.5.4 2023-02-09 18:11:48 +09:00
syuilo
0b269e79fd i/notificationsのレートリミットを緩和 2023-02-09 18:11:11 +09:00
syuilo
6159cfd138 enhance(client): improve api error handling 2023-02-09 18:07:51 +09:00
syuilo
6a5bbd335b Update CHANGELOG.md 2023-02-09 18:03:04 +09:00
syuilo
39e269db8c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 18:01:15 +09:00
syuilo
70fe23a3ce fix(client): validate url to improve security 2023-02-09 18:01:12 +09:00
KOKO
a6a8a7fb85 fix: dateの初期値が正常に入らない時がある (#9827)
* fix: dateの初期値が正常に入らない時がある

* feat: datettime-localをとれるように

* chore: いらない差分を戻す
2023-02-09 17:54:30 +09:00
syuilo
6641b13b4c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 17:54:14 +09:00
syuilo
5136b05c9b New translations ja-JP.yml (Spanish) (#9839) 2023-02-09 17:53:21 +09:00
syuilo
803c2144f4 Update about-misskey.vue 2023-02-09 17:44:18 +09:00
syuilo
b69a079514 lint 2023-02-09 17:36:16 +09:00
syuilo
2aa800cd55 Update about-misskey.vue 2023-02-09 17:34:45 +09:00
tamaina
6e61a36d05 i/notificationsのレートリミットを緩和
SubwayTooterのバグ対策でレートリミットを設定していたが、通常の使い方でも引っかかることもあるため緩和
2023-02-09 08:32:42 +00:00
tamaina
f80bf1fb1c perf: renderBaseでCache-Controlを300秒から30秒に 2023-02-09 08:19:12 +00:00
syuilo
d465e85239 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 17:03:21 +09:00
tamaina
deed25a2ff Fix #9842 2023-02-09 08:00:45 +00:00
tamaina
a486716520 perf: renderBaseでCache-Controlを15秒から300秒に 2023-02-09 07:49:39 +00:00
syuilo
2361e11e98 Update about-misskey.vue 2023-02-09 16:42:22 +09:00
syuilo
cd1f2adca7 🎨 2023-02-09 13:21:11 +09:00
syuilo
a558767b7a Merge branch 'develop' 2023-02-09 11:54:49 +09:00
syuilo
399ce9b999 13.5.3 2023-02-09 11:54:41 +09:00
syuilo
a94a0b5b0b New Crowdin updates (#9838)
* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Japanese, Kansai)
2023-02-09 11:52:08 +09:00
syuilo
76faec2115 refactor: fix types 2023-02-09 11:46:08 +09:00
syuilo
33c4e57994 refactor: fix types 2023-02-09 11:42:55 +09:00
syuilo
bc23496998 refactor: fix types 2023-02-09 11:31:40 +09:00
syuilo
d35ad95c18 refactor: fix types 2023-02-09 11:03:40 +09:00
syuilo
5facd11592 refactor: fix types 2023-02-09 11:02:37 +09:00
syuilo
e1e885d6b2 refactor: fix types 2023-02-09 10:55:15 +09:00
syuilo
5b6695114f refactor: fix types 2023-02-09 10:50:53 +09:00
syuilo
71dd7f89e9 clean up 2023-02-09 10:47:03 +09:00
syuilo
21331e53fe refactor: fix types 2023-02-09 10:46:01 +09:00
syuilo
7afee5977f feat(client): add channel column to deck 2023-02-09 10:35:28 +09:00
syuilo
d195b0dec7 refactor(client): use css modules 2023-02-09 10:11:33 +09:00
syuilo
8a95e850ad Update ROADMAP.md 2023-02-09 09:54:30 +09:00
syuilo
a4d74d7d7e 🎨 2023-02-09 09:48:35 +09:00
syuilo
256e0db36d 多分 #9815 2023-02-09 09:33:46 +09:00
syuilo
d593c1358a 🎨 2023-02-09 09:32:39 +09:00
syuilo
1ff14d81c1 update deps 2023-02-09 09:25:31 +09:00
syuilo
4369d12eec Merge branch 'develop' 2023-02-08 20:17:24 +09:00
syuilo
91cc033eb5 13.5.2 2023-02-08 20:17:13 +09:00
syuilo
57543e6b44 fix(client): ログイントークンの再生成が出来ない
Fix #9822
2023-02-08 20:12:44 +09:00
syuilo
a1b8cd15c4 fix(client): register_note_view_interruptor not working
Fix #9826
2023-02-08 20:11:53 +09:00
syuilo
73f06e591a revert: 650187deaf 2023-02-08 20:07:19 +09:00
tamaina
6f7cfa82b5 fix(client): 通知のノート表示で_nowrapが効いていない問題を修正
Fix #9834
2023-02-08 09:50:34 +00:00
syuilo
ff97a003d1 Merge branch 'develop' 2023-02-08 18:14:27 +09:00
syuilo
53c92e3e23 13.5.1 2023-02-08 18:14:17 +09:00
syuilo
13d13bc2f6 fix broken component 2023-02-08 18:13:45 +09:00
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
156 changed files with 2358 additions and 1354 deletions

View File

@@ -130,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

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

@@ -8,6 +8,89 @@
You should also include the user name that made the change.
-->
## 13.5.6 (2023/02/10)
### Improvements
- 非ログイン時にMiAuthを踏んだ際にMiAuthであることを表示する
- /auth/のUIをアップデート
- 利用規約同意UIの調整
- クロップ時の質問を分かりやすく
### Bugfixes
- fix: prevent clipping audio plyr's tooltip
## 13.5.4 (2023/02/09)
### Improvements
- Server: UIのHTMLートなどの特別なページを除くのキャッシュ時間を15秒から30秒に
- i/notificationsのレートリミットを緩和
### Bugfixes
- fix(client): validate url to improve security
- fix(client): dateの初期値が正常に入らない時がある
## 13.5.3 (2023/02/09)
### Improvements
- Client: デッキにチャンネルカラムを追加
## 13.5.2 (2023/02/08)
### Changes
- Revert: perf(client): do not render custom emojis in user names
### Bugfixes
- Client: register_note_view_interruptor not working
- Client: ログイントークンの再生成が出来ない
## 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)

View File

@@ -121,7 +121,7 @@ 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`.

View File

@@ -29,6 +29,7 @@ ARG NODE_ENV=production
RUN git submodule update --init
RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner
@@ -41,10 +42,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg tini \
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
@@ -58,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

@@ -6,16 +6,13 @@ Also, the later tasks are more indefinite and are subject to change as developme
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
- Make the number of type errors zero (backend)
- Probably need to switch some libraries to others that make it difficult to reduce type errors
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
- Improve CI
- Fix tests
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
- Add more tests
- May need to implement a mechanism that allows for DI
- ~~May need to implement a mechanism that allows for DI~~ → Done ✔️
- https://github.com/misskey-dev/misskey/pull/9085
- Measure coverage
- ~~Measure coverage~~ → Done ✔️
- https://github.com/misskey-dev/misskey/pull/9081
- Improve documentation
- Refactoring

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

@@ -1345,5 +1345,6 @@ _deck:
tl: "الخيط الزمني"
antenna: "الهوائيات"
list: "القوائم"
channel: "القنوات"
mentions: "الإشارات"
direct: "مباشرة"

View File

@@ -1441,5 +1441,6 @@ _deck:
tl: "টাইমলাইন"
antenna: "অ্যান্টেনা"
list: "লিস্ট"
channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি"

View File

@@ -804,4 +804,5 @@ _deck:
tl: "Časová osa"
antenna: "Antény"
list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?"
selectList: "Liste auswählen"
selectChannel: "Kanal auswählen"
selectAntenna: "Antenne auswählen"
selectWidget: "Widget auswählen"
editWidgets: "Widgets bearbeiten"
@@ -256,6 +257,8 @@ noMoreHistory: "Kein weiterer Verlauf vorhanden"
startMessaging: "Neuen Chat erstellen"
nUsersRead: "Von {n} Benutzern gelesen"
agreeTo: "Ich stimme {0} zu"
agreeBelow: "Ich stimme Untenstehendem zu"
basicNotesBeforeCreateAccount: "Wichtige Infos"
tos: "Nutzungsbedingungen"
start: "Anfangen"
home: "Startseite"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgef
rateLimitExceeded: "Versuchsanzahl überschritten"
cropImage: "Bild zuschneiden"
cropImageAsk: "Möchtest du das Bild zuschneiden?"
cropYes: "Zuschneiden"
cropNo: "Unbearbeitet verwenden"
file: "Datei"
recentNHours: "Letzten {n} Stunden"
recentNDays: "Letzten {n} Tage"
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes
preset: "Vorlage"
selectFromPresets: "Aus Vorlagen wählen"
achievements: "Errungenschaften"
gotInvalidResponseError: "Ungültige Antwort des Servers"
gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal."
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@@ -1195,6 +1202,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"
@@ -1590,12 +1600,15 @@ _permissions:
"read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge lesen"
"write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge bearbeiten"
_auth:
shareAccessTitle: "Verteilung von App-Berechtigungen"
shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?"
shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?"
permission: "{name} fordert folgende Berechtigungen"
permissionAsk: "Diese Anwendung fordert folgende Berechtigungen"
pleaseGoBack: "Bitte kehre zur Anwendung zurück"
callback: "Es wird zur Anwendung zurückgekehrt"
denied: "Zugriff verweigert"
pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren."
_antennaSources:
all: "Alle Notizen"
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
@@ -1866,5 +1879,6 @@ _deck:
tl: "Chronik"
antenna: "Antennen"
list: "Listen"
channel: "Kanal"
mentions: "Erwähnungen"
direct: "Direktnachrichten"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
suspendConfirm: "Are you sure that you want to suspend this account?"
unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
selectList: "Select a list"
selectChannel: "Select a channel"
selectAntenna: "Select an antenna"
selectWidget: "Select a widget"
editWidgets: "Edit widgets"
@@ -256,6 +257,8 @@ noMoreHistory: "There is no further history"
startMessaging: "Start a new chat"
nUsersRead: "read by {n}"
agreeTo: "I agree to {0}"
agreeBelow: "I agree to the below"
basicNotesBeforeCreateAccount: "Important notes"
tos: "Terms of Service"
start: "Begin"
home: "Home"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "Could not fetch account information"
rateLimitExceeded: "Rate limit exceeded"
cropImage: "Crop image"
cropImageAsk: "Do you want to crop this image?"
cropYes: "Crop"
cropNo: "Use as-is"
file: "File"
recentNHours: "Last {n} hours"
recentNDays: "Last {n} days"
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "This action cannot be performed temporarily
preset: "Preset"
selectFromPresets: "Choose from presets"
achievements: "Achievements"
gotInvalidResponseError: "Invalid server response"
gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later."
_achievements:
earnedAt: "Unlocked at"
_types:
@@ -1195,6 +1202,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"
@@ -1590,12 +1600,15 @@ _permissions:
"read:gallery-likes": "View your list of liked gallery posts"
"write:gallery-likes": "Edit your list of liked gallery posts"
_auth:
shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
permission: "{name} requests the following permissions"
permissionAsk: "This application requests the following permissions"
pleaseGoBack: "Please go back to the application"
callback: "Returning to the application"
denied: "Access denied"
pleaseLogin: "Please log in to authorize applications."
_antennaSources:
all: "All notes"
homeTimeline: "Notes from followed users"
@@ -1866,5 +1879,6 @@ _deck:
tl: "Timeline"
antenna: "Antennas"
list: "List"
channel: "Channel"
mentions: "Mentions"
direct: "Direct notes"

View File

@@ -56,7 +56,7 @@ reply: "Responder"
loadMore: "Ver más"
showMore: "Ver más"
showLess: "Cerrar"
youGotNewFollower: "te ha seguido"
youGotNewFollower: "ahora te sigue"
receiveFollowRequest: "Recibiste una solicitud de seguimiento"
followRequestAccepted: "La solicitud de seguimiento fue aceptada"
mention: "Menciones"
@@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
suspendConfirm: "¿Quiere suspender esta cuenta?"
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista"
selectChannel: "Seleccionar canal"
selectAntenna: "Seleccionar antena"
selectWidget: "Seleccionar widget"
editWidgets: "Editar widgets"
@@ -256,6 +257,8 @@ noMoreHistory: "El historial se ha acabado"
startMessaging: "Iniciar chat"
nUsersRead: "Leído por {n} personas"
agreeTo: "De acuerdo con {0}"
agreeBelow: "Estoy de acuerdo con lo siguiente"
basicNotesBeforeCreateAccount: "Notas básicas"
tos: "Términos de uso"
start: "Comenzar"
home: "Inicio"
@@ -939,6 +942,8 @@ cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se
preset: "Predefinido"
selectFromPresets: "Escoger desde predefinidos"
achievements: "Logros"
gotInvalidResponseError: "Respuesta del servidor inválida"
gotInvalidResponseErrorDescription: "Puede que el servidor esté caído o en mantenimiento. Favor de intentar más tarde"
_achievements:
earnedAt: "Desbloqueado el"
_types:
@@ -1195,6 +1200,9 @@ _role:
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"
@@ -1590,12 +1598,15 @@ _permissions:
"read:gallery-likes": "Ver favoritos de la galería"
"write:gallery-likes": "Editar favoritos de la galería"
_auth:
shareAccessTitle: "Permisos de la aplicación"
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?"
permission: "{name} solicita los siguientes permisos"
permissionAsk: "Esta aplicación requiere los siguientes permisos"
pleaseGoBack: "Por favor, vuelve a la aplicación"
callback: "Volviendo a la aplicación"
denied: "Acceso denegado"
pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación"
_antennaSources:
all: "Todas las notas"
homeTimeline: "Notas de los usuarios que sigues"
@@ -1866,5 +1877,6 @@ _deck:
tl: "Linea de tiempo"
antenna: "Antenas"
list: "Listas"
channel: "Canal"
mentions: "Menciones"
direct: "Mensaje directo"

View File

@@ -1541,5 +1541,6 @@ _deck:
tl: "Fil"
antenna: "Antennes"
list: "Listes"
channel: "Canaux"
mentions: "Mentions"
direct: "Direct"

View File

@@ -1673,5 +1673,6 @@ _deck:
tl: "Linimasa"
antenna: "Antena"
list: "Daftar"
channel: "Kanal"
mentions: "Sebutan"
direct: "Langsung"

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à"
@@ -1866,5 +1869,6 @@ _deck:
tl: "Timeline"
antenna: "Antenne"
list: "Liste"
channel: "Canale"
mentions: "Menzioni"
direct: "Diretta"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集"
@@ -256,6 +257,8 @@ noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを開始"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agreeBelow: "下記に同意する"
basicNotesBeforeCreateAccount: "基本的な注意事項"
tos: "利用規約"
start: "始める"
home: "ホーム"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "アカウント情報の取得に失敗しま
rateLimitExceeded: "レート制限を超えました"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしますか?"
cropYes: "クロップする"
cropNo: "そのまま使う"
file: "ファイル"
recentNHours: "直近{n}時間"
recentNDays: "直近{n}日"
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため
preset: "プリセット"
selectFromPresets: "プリセットから選択"
achievements: "実績"
gotInvalidResponseError: "サーバーの応答が無効です"
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
_achievements:
earnedAt: "獲得日時"
@@ -1148,7 +1155,7 @@ _achievements:
description: "ここをクリックした"
_justPlainLucky:
title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得"
description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo:
title: "神様コンプレックス"
description: "名前を syuilo に設定した"
@@ -1184,7 +1191,7 @@ _role:
description: "ロールの説明"
permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット"
assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル"
conditional: "コンディショナル"
@@ -1197,6 +1204,9 @@ _role:
baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"
@@ -1622,12 +1632,15 @@ _permissions:
"write:gallery-likes": "ギャラリーのいいねを操作する"
_auth:
shareAccessTitle: "アプリへのアクセス許可"
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
permission: "{name}は次の権限を要求しています"
permissionAsk: "このアプリは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻ってやっていってください"
callback: "アプリケーションに戻っています"
denied: "アクセスを拒否しました"
pleaseLogin: "アプリケーションにアクセス許可を与えるには、ログインが必要です。"
_antennaSources:
all: "全てのノート"
@@ -1919,5 +1932,6 @@ _deck:
tl: "タイムライン"
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "あなた宛て"
direct: "ダイレクト"

View File

@@ -46,7 +46,7 @@ copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このノートをほかして書き直すんかこのートへのリアクション、Renote、返信も全部消えてまうで。"
deleteAndEditConfirm: "このノートをほかしてもっかい直すこのートへのリアクション、Renote、返信も全部消えるんやけどそれでもええん?"
addToList: "リストに入れたる"
sendMessage: "メッセージを送る"
copyRSS: "RSSをコピー"
@@ -89,7 +89,7 @@ serverIsDead: "サーバーからの応答がないで。もうちょい待っ
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
enterListName: "リスト名を入れてや"
privacy: "プライバシー"
makeFollowManuallyApprove: "自分が認めた人だけがこのアカウントをフォローできるようにする"
makeFollowManuallyApprove: "他人のフォローは許可してからや!"
defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー"
followRequest: "フォローを頼む"
@@ -129,6 +129,7 @@ unblockConfirm: "ブロックやめたるってほんまか?"
suspendConfirm: "凍結してしもうてええか?"
unsuspendConfirm: "解凍するけどええか?"
selectList: "リストを選ぶ"
selectChannel: "チャンネルを選ぶ"
selectAntenna: "アンテナを選ぶ"
selectWidget: "ウィジェットを選ぶ"
editWidgets: "ウィジェットをいじる"
@@ -256,6 +257,8 @@ noMoreHistory: "これより過去の履歴はあらへんで"
startMessaging: "チャットやるで"
nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで"
agreeBelow: "下記に同意したる"
basicNotesBeforeCreateAccount: "よう読んでやってや"
tos: "利用規約"
start: "始める"
home: "ホーム"
@@ -300,7 +303,7 @@ avatar: "アイコン"
banner: "バナー"
nsfw: "閲覧注意"
whenServerDisconnected: "サーバーとの接続が切れたとき"
disconnectedFromServer: "サーバーとの通信が切れたで"
disconnectedFromServer: "サーバーが機嫌悪いねん"
reload: "リロード"
doNothing: "何もせんとく"
reloadConfirm: "リロードしてええか?"
@@ -673,8 +676,8 @@ sentReactionsCount: "リアクションした数やで"
receivedReactionsCount: "リアクションされた数"
pollVotesCount: "アンケートに投票した数"
pollVotedCount: "アンケートに投票された数"
yes: "はい"
no: "いいえ"
yes: "ええで"
no: "あかんで"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量やで"
noCrawle: "クローラーによるインデックスを拒否するで"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "アカウントの取得に失敗したみた
rateLimitExceeded: "レート制限が超えたみたいやで"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしたってええか?"
cropYes: "切り抜いたる"
cropNo: "切り抜かへん"
file: "ファイル"
recentNHours: "直近{n}時間"
recentNDays: "直近{n}日"
@@ -938,6 +943,37 @@ cannotPerformTemporary: "一時的に利用できへんで"
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
preset: "プリセット"
selectFromPresets: "プリセットから選ぶ"
achievements: "実績"
gotInvalidResponseError: "サーバー黙っとるわ、知らんけど"
gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。"
_achievements:
earnedAt: "貰った日ぃ"
_types:
_notes1:
title: "まいど!"
description: "初めてノート投稿したった"
_notes10:
title: "ノートの天保山"
_notes100:
title: "ノートの真田山"
_notes500:
title: "ノートの生駒山"
_notes5000:
title: "箕面の滝からノート"
_login3:
flavor: "今日からワシはミスキストやで"
_iLoveMisskey:
title: "Misskey好きやねん"
_foundTreasure:
title: "なんでも鑑定団"
_client30min:
title: "ねんね"
_noteDeletedWithin1min:
title: "*おおっと*"
_open3windows:
title: "マド開けすぎ"
_driveFolderCircularReference:
title: "環状線"
_role:
new: "ロールの作成"
edit: "ロールの編集"
@@ -1355,10 +1391,12 @@ _permissions:
_auth:
shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?"
shareAccessAsk: "アカウントのアクセスを許可してもええか?"
permission: "{name}に次の権限つけたってやって"
permissionAsk: "このアプリは次の権限を要求しとるで"
pleaseGoBack: "アプリケーションに戻ってええよ"
callback: "アプリケーションに戻っとるで"
denied: "アクセスを拒否ったで"
pleaseLogin: "アプリにアクセスさせるんやったら、ログインしてや。"
_antennaSources:
all: "みんなのノート"
homeTimeline: "フォローしとるユーザーのノート"
@@ -1587,6 +1625,7 @@ _notification:
pollEnded: "アンケートの結果が出たみたいや"
unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
achievementEarned: "実績を獲得しとるで"
_types:
all: "すべて"
follow: "フォロー"
@@ -1628,5 +1667,6 @@ _deck:
tl: "タイムライン"
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "あんた宛て"
direct: "ダイレクト"

View File

@@ -1866,5 +1866,6 @@ _deck:
tl: "타임라인"
antenna: "안테나"
list: "리스트"
channel: "채널"
mentions: "받은 멘션"
direct: "다이렉트"

244
locales/lo-LA.yml Normal file
View File

@@ -0,0 +1,244 @@
---
_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: "ສົ່ງຂໍ້ຄວາມ"
copyRSS: "ສຳເນົາ RSS"
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
reply: "ຕອບ​ໄປ​ທີ"
loadMore: "ໂຫຼດເພີ່ມເຕີມ"
showMore: "ໂຫຼດເພີ່ມເຕີມ"
showLess: "ປິດ"
youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ"
receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ"
followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ"
mention: "ໄດ້ກ່າວມາ"
mentions: "ກ່າວເຖິງ"
directNotes: "ໂດຍກົງຫມາຍເຫດ"
importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ"
import: "ນຳເຂົ້າ"
export: "ນຳອອກ"
files: "ໄຟລ໌"
download: "ດາວໂຫລດ"
driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? ບັນທຶກທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ"
unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?"
exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ"
importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ"
lists: "ລາຍການ"
noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
note: "ບັນທຶກ"
notes: "ບັນທຶກ"
following: "ກຳລັງຕິດຕາມ"
followers: "ຜູ້ຕິດຕາມ"
followsYou: "ຕິດ​ຕາມ​ເຈົ້າ"
createList: "ສ້າງລາຍຊື່"
manageLists: "ການບໍລິຫານບັນຊີລາຍການ"
error: "ຂໍ້ຜິດພາດ"
somethingHappened: "​ອຸຍ, ມີ​ບາງ​ຢ່າງ​ຜິ​ດ​ພາດ"
retry: "ລອງໃຫມ່"
pageLoadError: "ເກີດຄວາມຜິດພາດໃນການໂຫລດໜ້ານີ້"
pageLoadErrorDescription: "ປົກກະຕິແລ້ວມັນເກີດຈາກຄວາມຜິດພາດເຄືອຂ່າຍ ຫຼື cache ຂອງຕົວທ່ອງເວັບ ລອງລຶບລ້າງແຄດແລ້ວລອງໃໝ່ພາຍຫຼັງສອງສາມນາທີ"
serverIsDead: "ເຊີບເວີນີ້ບໍ່ຕອບສະໜອງ ກະລຸນາລໍຖ້າຈັກໜ່ອຍແລ້ວລອງໃໝ່ອີກຄັ້ງ"
youShouldUpgradeClient: "ເພື່ອເບິ່ງໜ້ານີ້, ກະລຸນາໂຫຼດຂໍ້ມູນຄືນໃໝ່ເພື່ອອັບເດດລູກຄ້າຂອງທ່ານ"
enterListName: "ໃສ່ຊື່ສຳລັບລາຍຊື່"
privacy: "ຄວາມເປັນສ່ວນຕົວ"
makeFollowManuallyApprove: "ປະຕິບັດຕາມການຮ້ອງຂໍຮຽກຮ້ອງໃຫ້ມີການອະນຸມັດ"
defaultNoteVisibility: "ເປັນຄ່າເລີ່ມຕົ້ນ"
follow: "ກຳລັງຕິດຕາມ"
followRequest: "ສົ່ງ​ການ​ຮ້ອງ​ຂໍ​ປະ​ຕິ​ບ​ຕາມ​"
followRequests: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍ"
unfollow: "ເຊົາຕິດຕາມ"
followRequestPending: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ລໍຖ້າຢູ່"
enterEmoji: "ປ້ອນອີໂມຈິ"
renote: "Renote"
unrenote: "ເລີກ Renote"
renoted: "ເກັບບັນທຶກໄວ້"
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
you: "ເຈົ້າ"
clickToShow: "ກົດເພື່ອສະແດງໃຫ້ເຫັນ"
sensitive: "NSFW"
add: "ເພີ່ມ"
reaction: "ປະຕິກິລິຍາ"
reactions: "ປະຕິກິລິຍາ"
mute: "ປີດສຽງ"
unmute: "ເປີດສຽງ"
block: "ບ໋ອກ"
unblock: "ຍົກເລີກກາຮົບລັອກ"
suspend: "ລະງັບ"
unsuspend: "ເຊົາ​ລະ​ງັບ"
selectList: "ເລືອກບັນຊີລາຍການ"
selectWidget: "ເລືອກວິກເຈັດ"
editWidgets: "ແກ້ໄຂ Widget"
editWidgetsExit: "ສຳເລັດແລ້ວ"
customEmojis: "ອີໂມຈິແບບກຳນົດເອງ"
emoji: "ອີໂມຈິ"
emojis: "ອີໂມຈິ"
emojiName: "ຊື່ Emoji"
emojiUrl: "URL ອີໂມຈິ"
addEmoji: "ຕື່ມອີໂມຈິ"
flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ"
flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ"
flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ"
flagShowTimelineReplies: "ສະແດງການຕອບກັບໃນທາມລາຍ"
flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບຂອງຜູ້ໃຊ້ຕໍ່ກັບບັນທຶກຂອງຜູ້ໃຊ້ອື່ນໃນທາມລາຍຖ້າເປີດໃຊ້ງານ"
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
addAccount: "ເພີ່ມບັນຊີ"
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
general: "ທົ່ວໄປ"
wallpaper: "ພາບພື້ນຫລັງ"
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
instances: "ອີນສະແຕນ"
instanceInfo: "ອີນສະແຕນ"
statistics: "ສະຖິຕິ"
clearQueue: "ລ້າງຄິວ"
clearCachedFiles: "ລຶບລ້າງແຄສ"
editProfile: "ແກ້ໄຂໂປຣໄຟລ໌"
done: "ສຳເລັດ"
processing: "ກຳລັງປະມວນຜົນ"
preview: "ສະແດງເປັນຕົວຢ່າງ"
default: "ຄ່າເລີ່ມຕົ້ນ"
blocked: "ບລັອກແລ້ວ "
all: "ທັງໝົດ"
subscribing: "ສະໝັກສະມາຊິກແລັວ"
publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່"
notResponding: "ບໍ່ຕອບສະໜອງ"
instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ"
instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​"
changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
featured: "ໄຮໄລທ໌"
announcements: "ປະກາດ"
remove: "ລຶບ"
messaging: "ແຊ໋ດ"
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
home: "ໜ້າຫຼັກ"
images: "ຮູບພາບ"
birthday: "ວັນເກີດ"
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
location: "ທີ່ຕັ້ງ"
theme: "ແທ໋ມ"
light: "ສະຫວ່າງ"
dark: "ມືດ"
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
darkThemes: "ຮູບແບບສີສັນມືດ"
fileName: "ຊື່ໄຟລ໌"
selectFile: "ເລືອກໄຟລ໌"
selectFiles: "ເລືອກໄຟລ໌"
nsfw: "NSFW"
accept: "ອະນຸຍາດ"
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
userList: "ລາຍການ"
smtpUser: "ຊື່ຜູ້ໃຊ້"
smtpPass: "ລະຫັດຜ່ານ"
clearCache: "ລຶບລ້າງແຄສ"
user: "ຜູ້ໃຊ້ຕ່າງໆ"
searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌"
_email:
_follow:
title: "ໄດ້ຕິດຕາມທ່ານ"
_mfm:
mention: "ໄດ້ກ່າວມາ"
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
emoji: "ອີໂມຈິແບບກຳນົດເອງ"
search: "ຄົ້ນຫາ"
_theme:
keys:
mention: "ໄດ້ກ່າວມາ"
renote: "Renote"
_sfx:
note: "ບັນທຶກ"
notification: "ການແຈ້ງເຕືອນ"
chat: "ແຊ໋ດ"
_widgets:
profile: "ໂພຼຟາຍ"
instanceInfo: "ອີນສະແຕນ"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
_userList:
chooseList: "ເລືອກບັນຊີລາຍການ"
_cw:
show: "ໂຫຼດເພີ່ມເຕີມ"
_visibility:
home: "ໜ້າຫຼັກ"
followers: "ຜູ້ຕິດຕາມ"
_profile:
username: "ຊື່ຜູ້ໃຊ້"
_exportOrImport:
followingList: "ກຳລັງຕິດຕາມ"
muteList: "ປີດສຽງ"
blockingList: "ບ໋ອກ"
userLists: "ລາຍການ"
_timelines:
home: "ໜ້າຫຼັກ"
_pages:
blocks:
image: "ຮູບພາບ"
_notification:
youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ"
_types:
follow: "ກຳລັງຕິດຕາມ"
mention: "ໄດ້ກ່າວມາ"
renote: "Renote"
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
reaction: "ປະຕິກິລິຍາ"
_actions:
reply: "ຕອບ​ໄປ​ທີ"
renote: "Renote"
_deck:
_columns:
notifications: "ການແຈ້ງເຕືອນ"
tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"

View File

@@ -1438,5 +1438,6 @@ _deck:
tl: "Oś czasu"
antenna: "Anteny"
list: "Listy"
channel: "Kanały"
mentions: "Wspomnienia"
direct: "Bezpośredni"

View File

@@ -721,4 +721,5 @@ _deck:
tl: "Cronologie"
antenna: "Antene"
list: "Liste"
channel: "Canale"
mentions: "Mențiuni"

View File

@@ -1845,5 +1845,6 @@ _deck:
tl: "Лента"
antenna: "Антенны"
list: "Списки"
channel: "Каналы"
mentions: "Упоминания"
direct: "Личное"

View File

@@ -1545,5 +1545,6 @@ _deck:
tl: "Časová os"
antenna: "Antény"
list: "Zoznam"
channel: "Kanály"
mentions: "Zmienky"
direct: "Priame poznámky"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต
suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?"
unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้"
selectList: "เลือกรายการ"
selectChannel: "เลือกแชนแนล"
selectAntenna: "เลือกเสาอากาศ"
selectWidget: "เลือกวิดเจ็ต"
editWidgets: "แก้ไขวิดเจ็ต"
@@ -939,6 +940,8 @@ cannotPerformTemporaryDescription: "การดําเนินการน
preset: "พรีเซ็ต"
selectFromPresets: "เลือกจากการพรีเซ็ต"
achievements: "ความสำเร็จ"
gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
@@ -1147,7 +1150,7 @@ _achievements:
description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
@@ -1182,7 +1185,7 @@ _role:
description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย"
assignTarget: "มอบหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง"
conditional: "มีเงื่อนไข"
@@ -1195,6 +1198,9 @@ _role:
baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"
@@ -1590,12 +1596,15 @@ _permissions:
"read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ"
"write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ"
_auth:
shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน"
shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?"
shareAccessAsk: "คุณแน่ใจแล้วจริงๆหรอว่าต้องการอนุญาตให้แอปพลิเคชันนี้เข้าถึงบัญชีของคุณแน่ใจแล้วหรอ?"
permission: "{name} ได้ขอสิทธิ์การเข้าถึงดังต่อไปนี้"
permissionAsk: "แอปพลิเคชันนี้ขอสิทธิ์ดังต่อไปนี้"
pleaseGoBack: "กรุณากลับไปที่แอปพลิเคชัน"
callback: "กำลังกลับไปที่แอปพลิเคชัน"
denied: "ปฏิเสธการเข้าใช้"
pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน"
_antennaSources:
all: "โน้ตทั้งหมด"
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
@@ -1866,5 +1875,6 @@ _deck:
tl: "ไทม์ไลน์"
antenna: "เสาอากาศ"
list: "รายการ"
channel: "แชนแนล"
mentions: "พูดถึง"
direct: "ไดเร็ค"

View File

@@ -1382,8 +1382,8 @@ _tutorial:
step1_1: "Ласкаво просимо!"
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
@@ -1689,5 +1689,6 @@ _deck:
tl: "Стрічка"
antenna: "Антени"
list: "Списки"
channel: "Канали"
mentions: "Згадки"
direct: "Особисте"

View File

@@ -1520,5 +1520,6 @@ _deck:
tl: "Bảng tin"
antenna: "Trạm phát sóng"
list: "Danh sách"
channel: "Kênh"
mentions: "Lượt nhắc"
direct: "Nhắn riêng"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "确定要解除拉黑吗?"
suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表"
selectChannel: "选择频道"
selectAntenna: "选择天线"
selectWidget: "选择小工具"
editWidgets: "编辑部件"
@@ -256,6 +257,8 @@ noMoreHistory: "没有更多的历史记录"
startMessaging: "添加聊天"
nUsersRead: "{n}人已读"
agreeTo: "勾选则表示已阅读并同意{0}"
agreeBelow: "同意以下观点"
basicNotesBeforeCreateAccount: "基本注意事项"
tos: "服务条款"
start: "开始"
home: "首页"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "获取账户信息失败"
rateLimitExceeded: "已超過速率限制"
cropImage: "剪裁图像"
cropImageAsk: "是否要裁剪图像?"
cropYes: "已裁剪"
cropNo: "就这样吧!"
file: "文件"
recentNHours: "最近{n}小时"
recentNDays: "最近{n}天"
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,
preset: "預設值"
selectFromPresets: "從預設值中選擇"
achievements: "成就"
gotInvalidResponseError: "服务器无应答"
gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。"
_achievements:
earnedAt: "达成时间"
_types:
@@ -1023,17 +1030,23 @@ _achievements:
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:
@@ -1086,6 +1099,7 @@ _achievements:
title: "信号塔"
description: "拥有超过500名关注者"
_followers1000:
title: "大影响家"
description: "拥有超过1000名关注者"
_collectAchievements30:
title: "成就收藏家"
@@ -1115,7 +1129,7 @@ _achievements:
description: "在0点发布一篇帖子"
flavor: "嘣 嘣 嘣 Biu——"
_selfQuote:
title: "自我提及"
title: "自我引用"
description: "引用了自己的帖子"
_htl20npm:
title: "流动的时间线"
@@ -1188,6 +1202,9 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级"
@@ -1583,12 +1600,15 @@ _permissions:
"read:gallery-likes": "读取喜欢的图片"
"write:gallery-likes": "操作喜欢的图片"
_auth:
shareAccessTitle: "应用程序授权许可"
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
permission: "{name}需要以下权限"
permissionAsk: "这个应用程序需要以下权限"
pleaseGoBack: "请返回到应用程序"
callback: "回到应用程序"
denied: "拒绝访问"
pleaseLogin: "在对应用进行授权许可之前,请先登录"
_antennaSources:
all: "所有帖子"
homeTimeline: "已关注用户的帖子"
@@ -1859,5 +1879,6 @@ _deck:
tl: "时间线"
antenna: "天线"
list: "列表"
channel: "频道"
mentions: "提及"
direct: "指定用户"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "確定解除封鎖此用戶?"
suspendConfirm: "確定凍結此帳號?"
unsuspendConfirm: "確定解凍此帳號?"
selectList: "選擇清單"
selectChannel: "選擇頻道"
selectAntenna: "選擇天線"
selectWidget: "選擇小工具"
editWidgets: "編輯小工具"
@@ -256,6 +257,8 @@ noMoreHistory: "沒有更多歷史紀錄"
startMessaging: "開始聊天"
nUsersRead: "{n}人已讀"
agreeTo: "我同意{0}"
agreeBelow: "同意以下內容"
basicNotesBeforeCreateAccount: "基本注意事項"
tos: "使用條款"
start: "開始"
home: "首頁"
@@ -326,7 +329,7 @@ connectService: "己連結"
disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
@@ -389,8 +392,8 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "審員"
moderation: "監察"
moderator: "審員"
moderation: "審查"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@@ -607,7 +610,7 @@ testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音"
instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼"
makeActive: "啟用"
display: "檢視"
@@ -861,6 +864,8 @@ failedToFetchAccountInformation: "取得帳戶資訊失敗"
rateLimitExceeded: "已超過速率限制"
cropImage: "圖片裁剪"
cropImageAsk: "要剪裁圖片嗎?"
cropYes: "裁剪"
cropNo: "使用原圖"
file: "檔案"
recentNHours: "過去{n}小時"
recentNDays: "過去{n}天"
@@ -939,6 +944,8 @@ cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無
preset: "預設值"
selectFromPresets: "從預設值中選擇"
achievements: "成就"
gotInvalidResponseError: "伺服器的回應無效"
gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。"
_achievements:
earnedAt: "獲得日期"
_types:
@@ -1181,7 +1188,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 +1202,11 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"
_priority:
low: "低"
@@ -1233,7 +1243,7 @@ _role:
or: "~或~"
not: "~否"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標"
@@ -1590,12 +1600,15 @@ _permissions:
"read:gallery-likes": "讀取喜歡的圖片"
"write:gallery-likes": "操作喜歡的圖片"
_auth:
shareAccessTitle: "應用程式的存取權限"
shareAccess: "要授權「“{name}”」存取您的帳戶嗎?"
shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?"
permission: "{name}要求以下的權限"
permissionAsk: "此應用程式需要以下權限"
pleaseGoBack: "請返回至應用程式"
callback: "回到應用程式"
denied: "拒絕訪問"
pleaseLogin: "必須登入以提供應用程式的存取權限。"
_antennaSources:
all: "全部貼文"
homeTimeline: "來自已追隨使用者的貼文"
@@ -1866,5 +1879,6 @@ _deck:
tl: "時間軸"
antenna: "天線"
list: "清單"
channel: "頻道"
mentions: "提及"
direct: "指定使用者"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.3.1",
"version": "13.5.6",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@7.24.3",
"packageManager": "pnpm@7.27.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -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",
@@ -54,8 +54,8 @@
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3",
"cypress": "12.5.1",
"eslint": "8.33.0",

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

@@ -23,9 +23,9 @@
"@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.1",
"@bull-board/fastify": "4.11.1",
"@bull-board/ui": "4.11.1",
"@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
@@ -34,9 +34,9 @@
"@fastify/multipart": "7.4.0",
"@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",
"@nestjs/common": "9.3.7",
"@nestjs/core": "9.3.7",
"@nestjs/testing": "9.3.7",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"accepts": "1.3.8",
@@ -46,7 +46,7 @@
"aws-sdk": "2.1295.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.4",
"bull": "4.10.2",
"bull": "4.10.3",
"cacheable-lookup": "6.1.0",
"cbor": "8.1.0",
"chalk": "5.2.0",
@@ -90,7 +90,7 @@
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.3.0",
"pureimage": "0.3.15",
"pureimage": "0.3.17",
"qrcode": "1.5.1",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
@@ -111,12 +111,12 @@
"stringz": "2.1.0",
"summaly": "2.7.0",
"systeminformation": "5.17.8",
"tinycolor2": "1.5.2",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typeorm": "0.3.12",
"typescript": "4.9.5",
"ulid": "2.3.0",
"unzipper": "0.10.11",
@@ -128,10 +128,10 @@
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.4.1",
"@jest/globals": "29.4.2",
"@redocly/openapi-core": "1.0.0-beta.123",
"@swc/cli": "0.1.61",
"@swc/core": "1.3.32",
"@swc/core": "1.3.34",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
@@ -145,11 +145,11 @@
"@types/ioredis": "4.28.10",
"@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1",
"@types/jsdom": "21.1.0",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
"@types/node": "18.11.18",
"@types/node": "18.13.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@@ -174,13 +174,13 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3",
"eslint": "8.33.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.4.1",
"jest-mock": "29.4.1"
"jest": "29.4.2",
"jest-mock": "29.4.2"
}
}

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

@@ -87,6 +87,8 @@ export type Mixin = {
userAgent: string;
clientEntry: string;
clientManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
};
export type Config = Source & Mixin;
@@ -135,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

@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
const ACHIEVEMENT_TYPES = [
export const ACHIEVEMENT_TYPES = [
'notes1',
'notes10',
'notes100',

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

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

@@ -120,7 +120,7 @@ export class CustomEmojiService {
const url = isLocal
? emojiUrl
: this.config.proxyRemoteFiles
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl;
return url;

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

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

@@ -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);
}
@@ -155,7 +155,7 @@ export class NoteReadService {
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions');
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
});
@@ -165,7 +165,7 @@ export class NoteReadService {
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
@@ -175,7 +175,7 @@ export class NoteReadService {
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventServie.publishMainStream(userId, 'readAllChannels');
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
@@ -200,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

@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js';
import type { SelectQueryBuilder } from 'typeorm';
import { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm';
@Injectable()
export class QueryService {
@@ -32,7 +32,7 @@ export class QueryService {
) {
}
public makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

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

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

@@ -252,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 {
@@ -283,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,
@@ -425,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,
@@ -459,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,

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

@@ -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);
}
@@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
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,
@@ -422,7 +427,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),
@@ -454,6 +459,7 @@ 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,

View File

@@ -5,7 +5,7 @@
* The getter will return a .bind version of the function
* and memoize the result against a symbol on the instance
*/
export function bindThis(target, key, descriptor) {
export function bindThis(target: any, key: string, descriptor: any) {
let fn = descriptor.value;
if (typeof fn !== 'function') {
@@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) {
});
return boundFn;
},
set(value) {
set(value: any) {
fn = value;
},
};

View File

@@ -45,7 +45,7 @@ export default class Logger {
}
const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
const l =
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
level === 'warning' ? chalk.yellow('WARN') :

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

@@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
bg.addColorStop(0, bgColors[0]);
bg.addColorStop(1, bgColors[1]);
ctx.fillStyle = bg;
ctx.fillStyle = bg as any;
ctx.beginPath();
ctx.fillRect(0, 0, size, size);

View File

@@ -11,10 +11,9 @@ export class I18n<T extends Record<string, any>> {
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
@bindThis
public t(key: string, args?: Record<string, any>): string {
try {
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
let str = key.split('.').reduce((o, i) => o[i], this.locale as any) as string;
if (args) {
for (const [k, v] of Object.entries(args)) {

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

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

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

@@ -1,3 +1,4 @@
import { IncomingMessage } from 'node:http';
import { Inject, Injectable } from '@nestjs/common';
import fastifyAccepts from '@fastify/accepts';
import httpSignature from '@peertube/http-signature';
@@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
@@ -97,7 +99,8 @@ export class ActivityPubServerService {
return;
}
this.queueService.inbox(request.body, signature);
// TODO: request.bodyのバリデーション
this.queueService.inbox(request.body as IActivity, signature);
reply.code(202);
}
@@ -413,20 +416,21 @@ export class ActivityPubServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addConstraintStrategy({
// addConstraintStrategy の型定義がおかしいため
(fastify.addConstraintStrategy as any)({
name: 'apOrHtml',
storage() {
const store = {};
const store = {} as any;
return {
get(key) {
get(key: string) {
return store[key] ?? null;
},
set(key, value) {
set(key: string, value: any) {
store[key] = value;
},
};
},
deriveConstraint(request, ctx) {
deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
return isAp ? 'ap' : 'html';
@@ -536,6 +540,7 @@ export class ActivityPubServerService {
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
} else {
reply.code(400);
return;
}
});

View File

@@ -137,38 +137,42 @@ export class FileServerService {
try {
if (file.state === 'remote') {
const convertFile = async () => {
if (file.fileRole === 'thumbnail') {
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
498,
280
);
} else if (file.mime.startsWith('video/')) {
return await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
let image: IImageStreamable | null = null;
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
return this.imageProcessingService.convertToWebpStream(
file.path,
2048,
2048,
{ ...webpDefault, lossless: true }
)
}
}
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
return {
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
}
}
if (!image) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
};
const image = await convertFile();
}
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
@@ -180,7 +184,6 @@ export class FileServerService {
}
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;
}
@@ -217,6 +220,23 @@ export class FileServerService {
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') {
@@ -235,8 +255,21 @@ export class FileServerService {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
if (
'emoji' in request.query ||
'avatar' in request.query ||
'static' in request.query ||
'preview' in request.query ||
'badge' in request.query
) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
}
let image: IImageStreamable | null = null;
if ('emoji' in request.query && isConvertibleImage) {
if ('emoji' in request.query || 'avatar' in request.query) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
@@ -246,7 +279,7 @@ export class FileServerService {
} else {
const data = sharp(file.path, { animated: !('static' in request.query) })
.resize({
height: 128,
height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
@@ -257,16 +290,11 @@ export class FileServerService {
type: 'image/webp',
};
}
} else if ('static' in request.query && isConvertibleImage) {
} else if ('static' in request.query) {
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
} else if ('preview' in request.query && isConvertibleImage) {
} else if ('preview' in request.query) {
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',
@@ -370,7 +398,7 @@ export class FileServerService {
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
{ state: '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'
@@ -392,6 +420,7 @@ export class FileServerService {
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
return {
...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
}

View File

@@ -106,7 +106,7 @@ export class ServerService {
}
}
const url = new URL('/proxy/emoji.webp', this.config.url);
const url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1');
@@ -166,6 +166,7 @@ export class ServerService {
return 'Verify succeeded!';
} else {
reply.code(404);
return;
}
});

View File

@@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval * factor,
duration: limitation.minInterval! * factor,
max: 1,
db: this.redisClient,
});
@@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration * factor,
max: limitation.max / factor,
duration: limitation.duration! * factor,
max: limitation.max! / factor,
db: this.redisClient,
});

View File

@@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
import { bindThis } from '@/decorators.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -131,7 +131,7 @@ export class SigninApiService {
createdAt: new Date(),
userId: user.id,
ip: request.ip,
headers: request.headers,
headers: request.headers as any,
success: false,
});

View File

@@ -25,7 +25,7 @@ export class SigninService {
}
@bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
setImmediate(async () => {
// Append signin history
const record = await this.signinsRepository.insert({
@@ -33,7 +33,7 @@ export class SigninService {
createdAt: new Date(),
userId: user.id,
ip: request.ip,
headers: request.headers,
headers: request.headers as any,
success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
@@ -41,25 +41,11 @@ export class SigninService {
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
});
if (redirect) {
//#region Cookie
reply.setCookie('igi', user.token!, {
path: '/',
// SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: this.config.url.startsWith('https'),
httpOnly: false,
});
//#endregion
reply.redirect(this.config.url);
} else {
reply.code(200);
return {
id: user.id,
i: user.token,
};
}
reply.code(200);
return {
id: user.id,
i: user.token,
};
}
}

View File

@@ -146,6 +146,7 @@ export class SignupApiService {
`To complete signup, please click this link: ${link}`);
reply.code(204);
return;
} else {
try {
const { account, secret } = await this.signupService.signup({
@@ -162,7 +163,7 @@ export class SignupApiService {
token: secret,
};
} catch (err) {
throw new FastifyReplyError(400, err);
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
}
}
}
@@ -195,7 +196,7 @@ export class SignupApiService {
return this.signinService.signin(request, reply, account as ILocalUser);
} catch (err) {
throw new FastifyReplyError(400, err);
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
}
}
}

View File

@@ -19,11 +19,13 @@ export const paramDef = {
name: { type: 'string' },
description: { type: 'string' },
color: { type: 'string', nullable: true },
target: { type: 'string' },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string', enum: ['manual', 'conditional'] },
condFormula: { type: 'object' },
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
policies: {
type: 'object',
@@ -33,11 +35,13 @@ export const paramDef = {
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'policies',
],
@@ -64,11 +68,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));

View File

@@ -27,11 +27,13 @@ export const paramDef = {
name: { type: 'string' },
description: { type: 'string' },
color: { type: 'string', nullable: true },
target: { type: 'string' },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string', enum: ['manual', 'conditional'] },
condFormula: { type: 'object' },
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
policies: {
type: 'object',
@@ -42,11 +44,13 @@ export const paramDef = {
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'policies',
],
@@ -73,11 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies,
});

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { getJsonSchema } from '@/core/chart/core.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import { schema } from '@/core/chart/charts/entities/per-user-notes.js';
import { schema } from '@/core/chart/charts/entities/per-user-pv.js';
export const meta = {
tags: ['charts', 'users'],

View File

@@ -27,7 +27,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return {
params: Object.entries(ep.params.properties ?? {}).map(([k, v]) => ({
name: k,
type: v.type.charAt(0).toUpperCase() + v.type.slice(1),
type: v.type ? v.type.charAt(0).toUpperCase() + v.type.slice(1) : 'string',
})),
};
});

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AchievementService } from '@/core/AchievementService.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
export const meta = {
requireCredential: true,
@@ -10,7 +10,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string' },
name: { type: 'string', enum: ACHIEVEMENT_TYPES },
},
required: ['name'],
} as const;

View File

@@ -15,8 +15,8 @@ export const meta = {
requireCredential: true,
limit: {
duration: 60000,
max: 15,
duration: 30000,
max: 30,
},
kind: 'read:notifications',

View File

@@ -181,6 +181,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
features: {
type: 'object',
optional: true, nullable: false,
@@ -307,6 +311,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,

View File

@@ -5,8 +5,8 @@ import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { AchievementService } from '@/core/AchievementService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['notes', 'favorites'],
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id,
});
if (note.userHost == null) {
if (note.userHost == null && note.userId !== me.id) {
this.achievementService.create(note.userId, 'myNoteFavorited1');
}
});

View File

@@ -1,6 +1,6 @@
import { Not } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
import type { IRemoteUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -77,9 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -93,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private userBlockingService: UserBlockingService,
) {
super(meta, paramDef, async (ps, me) => {
const createdAt = new Date();
@@ -109,11 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check blocking
if (note.userId !== me.id) {
const block = await this.blockingsRepository.findOneBy({
blockerId: note.userId,
blockeeId: me.id,
});
if (block) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
if (blocked) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}

View File

@@ -95,14 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
if (ps.tag) {
if (!safeForSql(ps.tag)) throw 'Injection';
if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
query.andWhere(new Brackets(qb => {
for (const tags of ps.query!) {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
if (!safeForSql(tag)) throw 'Injection';
if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
}
}));

View File

@@ -25,6 +25,8 @@ export interface InternalStreamTypes {
remoteUserUpdated: { id: User['id']; };
follow: { followerId: User['id']; followeeId: User['id']; };
unfollow: { followerId: User['id']; followeeId: User['id']; };
blockingCreated: { blockerId: User['id']; blockeeId: User['id']; };
blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; };
policiesUpdated: Role['policies'];
roleCreated: Role;
roleDeleted: Role;

View File

@@ -155,7 +155,7 @@ export class ClientServerService {
});
serverAdapter.setBasePath(bullBoardPath);
fastify.register(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion
fastify.register(fastifyView, {
@@ -337,7 +337,7 @@ export class ClientServerService {
const renderBase = async (reply: FastifyReply) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
img: meta.bannerUrl,
title: meta.name ?? 'Misskey',
@@ -372,6 +372,7 @@ export class ClientServerService {
return feed.atom1();
} else {
reply.code(404);
return;
}
});
@@ -384,6 +385,7 @@ export class ClientServerService {
return feed.rss2();
} else {
reply.code(404);
return;
}
});
@@ -396,6 +398,7 @@ export class ClientServerService {
return feed.json1();
} else {
reply.code(404);
return;
}
});

View File

@@ -33,7 +33,7 @@ export class UrlPreviewService {
private wrap(url?: string): string | null {
return url != null
? url.match(/^https?:\/\//)
? `${this.config.url}/proxy/preview.webp?${query({
? `${this.config.mediaProxy}/preview.webp?${query({
url,
preview: '1',
})}`
@@ -73,6 +73,14 @@ export class UrlPreviewService {
});
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) {
throw new Error('unsupported schema included');
}
summary.icon = this.wrap(summary.icon);
summary.thumbnail = this.wrap(summary.thumbnail);

View File

@@ -35,7 +35,8 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css')
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

View File

@@ -11,7 +11,7 @@ import FormData from 'form-data';
import { DataSource } from 'typeorm';
import got, { RequestError } from 'got';
import loadConfig from '../src/config/load.js';
import { entities } from '../src/postgre.js';
import { entities } from '@/postgres.js';
import type * as misskey from 'misskey-js';
const _filename = fileURLToPath(import.meta.url);

View File

@@ -12,7 +12,7 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.4",
"@tabler/icons-webfont": "2.1.2",
"@tabler/icons-webfont": "2.2.0",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.47",
"autobind-decorator": "2.4.0",
@@ -23,7 +23,7 @@
"canvas-confetti": "1.6.0",
"chart.js": "4.2.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "1.3.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.0",
"compare-versions": "5.0.1",
@@ -44,7 +44,7 @@
"punycode": "2.3.0",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.12.1",
"rollup": "3.14.0",
"s-age": "1.1.2",
"sanitize-html": "2.9.0",
"sass": "1.58.0",
@@ -55,7 +55,7 @@
"textarea-caret": "3.1.0",
"three": "0.149.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.5.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
@@ -74,7 +74,7 @@
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2",
"@types/node": "18.11.18",
"@types/node": "18.13.0",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.8.0",
"@types/seedrandom": "3.0.4",
@@ -83,8 +83,8 @@
"@types/uuid": "9.0.0",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"@vue/runtime-core": "3.2.47",
"cross-env": "7.0.3",
"cypress": "12.5.1",

View File

@@ -61,8 +61,6 @@ export async function signout() {
} catch (err) {}
//#endregion
document.cookie = 'igi=; path=/';
if (accounts.length > 0) login(accounts[0].token);
else unisonReload('/');
}

View File

@@ -107,6 +107,7 @@ onMounted(() => {
}
.iconFrame {
position: relative;
width: 58px;
height: 58px;
padding: 6px;

View File

@@ -1,6 +1,6 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
<code v-if="inline" :class="`language-${prismLang}`" style="overflow-wrap: anywhere;" v-html="html"></code>
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
</template>

View File

@@ -18,7 +18,7 @@
</div>
</Transition>
<div class="container">
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
<img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
</div>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<button class="_button" :class="$style.root" @click="toggle">
<button class="_button" :class="$style.root" @mousedown="toggle">
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
</button>

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