Compare commits

..

170 Commits

Author SHA1 Message Date
syuilo
c5d2dba28d Merge pull request #10608 from misskey-dev/develop
Release: 13.11.3
2023-04-13 12:18:07 +09:00
syuilo
2a7ba37996 [ci skip] improve readability 2023-04-13 09:09:29 +09:00
syuilo
3f57119aea [ci skip] remove outdated comment 2023-04-13 09:02:41 +09:00
syuilo
ddb1ab7fae 13.11.3 2023-04-13 08:54:47 +09:00
syuilo
8913e561db New Crowdin updates (#10585)
* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (Chinese Simplified)
2023-04-13 08:53:29 +09:00
syuilo
dcbaca4260 fix(backend): チャンネルのピン留めされたノートの順番が正しくない問題を修正
Fix #10541
2023-04-13 08:52:30 +09:00
syuilo
6839441ac6 🎨 2023-04-13 08:46:10 +09:00
syuilo
62b6c4d09b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-13 08:43:15 +09:00
syuilo
1a7e5fb865 Update about-misskey.vue 2023-04-13 08:43:06 +09:00
syuilo
272e0c874f Merge pull request #10606 from misskey-dev/EbiseLutica-patch-1
Update CHANGELOG.md
2023-04-13 08:35:14 +09:00
syuilo
78a2feb24c Update CHANGELOG.md 2023-04-13 08:34:25 +09:00
futchitwo
04511ac141 fix(server): アンテナとロールTLのuntil/sinceプロパティが動くように (#10605)
* fix(server): アンテナとロールTLのuntil/sinceプロパティが動くように

* fix
2023-04-13 08:33:36 +09:00
Ebise Lutica
d429f810a9 Update CHANGELOG.md 2023-04-13 00:31:22 +09:00
tamaina
4c0ef07f6f fix 2023-04-12 12:34:34 +00:00
tamaina
3ff5a5ae29 fix type in CustomEmojiService 2 2023-04-12 12:32:27 +00:00
tamaina
6ea057f8f8 fix type in CustomEmojiService 2023-04-12 12:09:28 +00:00
hutchisr
b7d056fb22 Use unique identifier for each follow request (#10600)
Co-authored-by: anemone <anemoneya@icloud.com>
2023-04-12 20:22:50 +09:00
syuilo
e3aeab8122 fix type 2023-04-12 17:02:54 +09:00
syuilo
72031e49fc Update CustomEmojiService.ts 2023-04-12 16:10:17 +09:00
syuilo
d06d1e8682 fix(backend): カスタム絵文字でリアクションできないことがある問題を修正 2023-04-12 16:07:58 +09:00
Nanashia
5c3a4a8224 test(backend): Add tests for users (#10546)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-04-12 13:20:16 +09:00
kakkokari-gtyih
49749b46c4 feat(server): Misskey Webでユーザーフレンドリーなエラーページを出す (#10590)
* (add) user-friendly error page

* Update CHANGELOG.md

* (add) cache-control header

* Add ClientLoggerService

* Log params and query

* remove error stack on client

* fix pug

* 文面を調整

* :art]

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-04-12 12:52:14 +09:00
syuilo
5d56799070 feat: role timeline
Resolve #10581
2023-04-12 11:40:08 +09:00
tamaina
81d2c5a4a7 enhance: カスタム絵文字関連の変更 (#9794)
* PackedNoteなどのemojisはプロキシしていないURLを返すように

* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用する

* update CHANGELOG.md

* fix changelog

* ??

* wip

* fix

* merge

* Update packages/frontend/src/scripts/media-proxy.ts

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* merge

* calc scale

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-04-12 10:58:56 +09:00
tamaina
0db88a5a3b refactor: サウンド関連の設定をpizzaxに移行 (#8105)
* enhane: unison-reloadに指定したパスに移動できるように

* null

* null

* feat: ログインするアカウントのIDをクエリ文字列で指定する機能

* null

* await?

* rename

* rename

* Update read.ts

* merge

* get-note-summary

* fix

* swパッケージに

* add missing packages

* fix getNoteSummary

* add webpack-cli

* ✌️

* remove plugins

* sw-inject分離したがテストしてない

* fix notification.vue

* remove a blank line

* disconnect intersection observer

* disconnect2

* fix notification.vue

* remove a blank line

* disconnect intersection observer

* disconnect2

* fix

* ✌️

* clean up config

* typesを戻した

* backend/src/web/index.ts

* notification-badges

* add scripts

* change create-notification.ts

* Update packages/client/src/components/notification.vue

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

* disconnect

* oops

* Failed to load the script unexpectedly回避
sw.jsとlib.tsを分離してみた

* truncate notification

* Update packages/client/src/ui/_common_/common.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* clean up

* clean up

* refactor

* キャッシュ対策

* Truncate push notification message

* fix

* wip

* clean up

* migration

* migration

* comment

* move soundConfigStore

* ✌️

* clean up

* クライアントがあったらストリームに接続しているということなので通知しない判定の位置を修正

* components/drive-file-thumbnail.vue

* components/drive-select-dialog.vue

* components/drive-window.vue

* merge

* fix

* remove reversi setting

* Service Workerのビルドにesbuildを使うようにする

* return createEmptyNotification()

* fix

* fix

* i18n.ts

* update

* ✌️

* remove ts-loader

* fix

* fix

* enhance: Service Workerを常に登録するように

* pollEnded

* pollEnded

* URLをsw.jsに戻す

* clean up

* clean up

* update sounds.vue

* update

* fix type

* ✌️

* ;v;

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-04-12 10:39:57 +09:00
tamaina
f9720e0e6e update CHANGELOG.md 2023-04-12 01:32:25 +00:00
syuilo
77f91d67b4 perf(backend): ノート作成時のアンテナ追加パフォーマンスを改善 2023-04-12 10:07:14 +09:00
Namekuji
da83322200 feat: queueing bulk follow/unfollow and block/unblock (#10544)
* wrap follow/unfollow and block/unblock as job queue

* create import job to follow in each iteration

* make relationship jobs concurrent

* replace to job queue if called repeatedly

* use addBulk to import

* omit stream when importing

* fix job caller

* use ThinUser instead of User to reduce redis memory consumption

* createImportFollowingToDbJobの呼び出し方を変える, 型補強

* Force ThinUser

* オブジェクト操作のみのメソッド名はgenerate...Data

* Force ThinUser in generateRelationshipJobData

* silent bulk unfollow at admin api endpoint

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-04-12 09:13:58 +09:00
futchitwo
b463490d9f Update CHANGELOG.md (#10591) 2023-04-12 02:08:41 +09:00
okayurisotto
5002effd65 Refactor sw (#10579)
* refactor(sw): remove dead code

* refactor(sw): remove dead code

* refactor(sw): remove dead code

* refactor(sw): remove dead code

* refactor(sw): remove dead code

* refactor(sw): remove dead code

* refactor(sw): 冗長な部分を変更

* refactor(sw): 使われていない煩雑な機能を削除

* refactor(sw): remove dead code

* refactor(sw): URL文字列の作成に`URL`を使うように

* refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処

* refactor(sw): `append` -> `set` in `URLSearchParams`

* refactor(sw): `any`の削除とそれに伴い露呈したエラーへの対処

* refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処

対処と言っても`throw`するだけ。いままでもこの状況ではエラーが投げられていたはずなので、この対処により新たな問題が起きることはないはず。

* refactor(sw): i18n loading

* refactor(sw): 型推論がうまくできる書き方に変更

`codes`が`(string | undefined)[]`から`string[]`になった

* refactor(sw): クエリ文字列の作成に`URLSearchParams`を使うように

* refactor(sw): `findClient`

* refactor(sw): `openClient`における`any`や`as`の書き換え

* refactor(sw): `openPost`における`any`の書き換え

* refactor(sw): `let` -> `const`

* refactor(sw): `any` -> `unknown`

* cleanup(sw): import

* cleanup(sw)

* cleanup(sw): `?.`

* cleanup(sw/.eslintrc.js)

* refactor(sw): `@typescript-eslint/explicit-function-return-type`

* refactor(sw): `@typescript-eslint/no-unused-vars`

* refactor(sw): どうしようもないところに`eslint-disable-next-line`を

* refactor(sw): `import/no-default-export`

* update operations.ts

* throw new Error

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-04-12 01:07:24 +09:00
syuilo
75b28d6782 Merge pull request #10578 from misskey-dev/develop
Release: 13.11.2
2023-04-11 15:51:07 +09:00
tamaina
35613fd642 fix(client): noPaging: true with gallery/featured 2023-04-11 06:49:19 +00:00
syuilo
5cabbd0eef Update CHANGELOG.md 2023-04-11 14:59:56 +09:00
syuilo
8b509f6c36 13.11.2 2023-04-11 14:58:48 +09:00
たーびん
2612bcd738 Update CHANGELOG.md (#10577)
#10555 の更新分
2023-04-11 14:57:37 +09:00
syuilo
de0577bc38 enhance(frontend): tweak post form style 2023-04-11 14:57:06 +09:00
syuilo
b192dc0774 New Crowdin updates (#10566)
* New translations ja-JP.yml (Chinese Traditional)

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

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

* New translations ja-JP.yml (Thai)

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Indonesian)
2023-04-11 14:39:50 +09:00
syuilo
43eee54f2d enhance(backend): APIパラメータサイズ上限を128kbから1mbに緩和 2023-04-11 14:28:40 +09:00
syuilo
59ca0d21a1 enhance(backend): APIパラメータサイズ上限を32kbから128kbに緩和
Fix #10574
2023-04-11 14:27:09 +09:00
syuilo
92356d02b9 Update CHANGELOG.md 2023-04-11 14:22:12 +09:00
syuilo
c10d591bd0 perf(backend): cache swSubscriptions 2023-04-11 14:20:16 +09:00
tamaina
3a90bcc03c sw: なんかもうめっちゃ変えた (#10570)
* sw: なんかいろいろ

* remove debug code

* never renotify

* update changelog.md
2023-04-11 14:11:39 +09:00
たーびん
f6dc100748 fix #10554 チャンネルの検索用ページとAPIの追加 (#10555)
* add channel search

* move  channel search to channel list page

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-04-11 07:42:27 +09:00
Chimorium
0702f9775a カスタム絵文字のキャッシュ時に"{}"が入ってしまう問題を修正 (#10573) 2023-04-11 07:39:46 +09:00
tamaina
838625edcd update CHANGELOG.md 2023-04-10 17:26:52 +00:00
tamaina
83bcdb8ede fix(client): Consider safe-area-inset-bottom on global widgets area
Fix #9052
2023-04-10 17:21:28 +00:00
tamaina
567c66567e fix(client): 🎨 fix MkEmojiPicker safe-area-inset-bottom
Fix https://github.com/misskey-dev/misskey/pull/10534
2023-04-10 16:46:58 +00:00
tamaina
da64273b43 chore(sw): use PascalCase 2023-04-10 12:10:06 +00:00
okayurisotto
6a23ffcce5 swのesbuildの更新とビルドスクリプトの更新 (#10549)
* cleanup(sw/build.js)

* fix(sw/build.js): `define`に真偽値を渡していた問題を修正

`define`では文字列を渡さなければならないので、`JSON.stringify`をするようにした。

* fix(sw/build.js): `string`が期待される`define`において`undefined`になる場合がある問題を修正

* update(sw): esbuild 0.17.15

* fixup! update(sw): esbuild 0.17.15

* fixup! fix(sw/build.js): `string`が期待される`define`において`undefined`になる場合がある問題を修正

コメントの文言を調整
2023-04-10 19:43:15 +09:00
syuilo
511dab0618 fix(frontend): webhook, 連携アプリ一覧でコンテンツが重複して表示される問題を修正
Fix #10564
2023-04-10 18:56:38 +09:00
syuilo
48e2523081 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-10 10:24:08 +09:00
e0431aed28 fix issue #10195 設定のバックアップ の「削除」の文字がない (#10559)
Co-authored-by: 藤 <nyaguri0417@gmail.com>
2023-04-10 10:23:56 +09:00
syuilo
29c9a7d71a enhance(frontend): 常に広告を見られるオプションを追加 2023-04-10 10:22:25 +09:00
syuilo
eba42230ee Update CHANGELOG.md 2023-04-10 10:12:05 +09:00
syuilo
f8315a40b4 Update CHANGELOG.md 2023-04-10 10:04:16 +09:00
syuilo
b5724d06b4 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-10 10:03:56 +09:00
syuilo
70a06e30d5 fix(backend): アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
Fix #10482
2023-04-10 10:03:53 +09:00
YS
3ec060f0dc MkContainer.vue i18n をtemplateから見えるように (#10560) 2023-04-10 08:31:06 +09:00
syuilo
39cf80e19f fix(backend): イベント用redis分離が上手く動かない問題を修正 2023-04-09 17:09:27 +09:00
syuilo
b56f4b27ee fix(backend): ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正 2023-04-09 17:01:03 +09:00
Acid Chicken (硫酸鶏)
2b19e1f732 test: add /@:acct stories (#10517)
* test: add `/@:acct` stories

* test: add mocks
2023-04-09 04:16:56 +00:00
syuilo
8b1362ab03 Merge pull request #10543 from misskey-dev/develop
Release: 13.11.1
2023-04-09 10:29:36 +09:00
syuilo
59d0d507d5 fix(backend): 連合しているインスタンスについて予期せず配送が全て停止されることがある問題を修正
Fix #10499
2023-04-09 10:19:57 +09:00
syuilo
86de46debf Update CHANGELOG.md 2023-04-09 09:53:29 +09:00
syuilo
1057da1556 add note 2023-04-09 09:53:02 +09:00
tamaina
9feb6b0f5b fix(server): リアクションできない問題をとりあえず修正 (#10529)
* fix(server): リアクションできない問題をとりあえず修正
Fix #10502

* Update packages/backend/src/core/CustomEmojiService.ts

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-04-09 09:52:19 +09:00
syuilo
269d1e72cb fix(frontend): ユーザープレビューが表示されない問題を修正
Fix #10540
2023-04-09 09:44:00 +09:00
syuilo
31ff3a22b7 13.11.1 2023-04-09 09:40:00 +09:00
syuilo
b9d022f164 New Crowdin updates (#10522)
* New translations ja-JP.yml (Swedish)

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

* New translations ja-JP.yml (Italian)

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

* New translations ja-JP.yml (Italian)

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

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)
2023-04-09 09:18:18 +09:00
syuilo
087da7643e Update CHANGELOG.md 2023-04-09 08:37:55 +09:00
syuilo
cb3a84adb4 チャンネルを新規作成できない問題を修正 2023-04-09 08:22:49 +09:00
syuilo
004ace396d さがすのローカルユーザー(ピンどめ)が無限に生成される問題を修正 2023-04-09 08:12:50 +09:00
syuilo
9ae2e87a46 Update CHANGELOG.md 2023-04-09 08:10:12 +09:00
syuilo
ad4006738b チャンネルのお気に入りが無限に読み込まれる問題を修正 2023-04-09 08:09:25 +09:00
taichan
f794f3ad0a fix(client): リスト、クリップが無限ロードされる現象の解決 (#10538)
* fix my-list infinite items loading

* update CHANGELOG.md

* fix my-clip infinite items loading
2023-04-09 08:03:29 +09:00
syuilo
b7ed3ddfdd fix(backend): 通知読み込みでエラーが発生する場合がある問題を修正 2023-04-09 08:02:52 +09:00
futchitwo
038365bf2d fix: redis から取得できないチャンネル投稿はDBから取得 (#10539) 2023-04-09 07:56:27 +09:00
syuilo
64597a2dab Update CHANGELOG.md 2023-04-09 04:56:36 +09:00
tamaina
d76220cc80 fix(server): IdService.parseを全てのidタイプに対応させるように (#10533)
* wip fix-id

* ✌️

* fix import
2023-04-09 04:41:06 +09:00
tamaina
7a33c5d2ee update CHANGELOG.md 2023-04-08 16:44:49 +00:00
Takeshi Kishi
e58b357918 fix(client): PWA時の絵文字ピッカーの位置をホームバーに重ならないように調整 (#10534)
* fix emoji picker padding

emoji picker bottoms are hidden by iPhone Home Bar.
To fix this, add safe-area padding

* update CHANGELOG
2023-04-09 01:37:45 +09:00
syuilo
a096f621cf Merge pull request #10506 from misskey-dev/develop
13.11.0
2023-04-08 21:27:21 +09:00
syuilo
789689e759 [ci skip] Update CHANGELOG.md 2023-04-08 21:26:43 +09:00
syuilo
2321214c98 disable account migration 2023-04-08 20:51:36 +09:00
syuilo
50a9b29c48 [ci skip] 13.11.0 2023-04-08 20:29:55 +09:00
syuilo
c5ccd826c8 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-08 20:03:35 +09:00
syuilo
ad9f72291f use for of 2023-04-08 20:03:32 +09:00
tamaina
1c158dc510 fix(client): Fix antenna list infinite items loading
https://github.com/misskey-dev/misskey/pull/10506#issuecomment-1500859975
2023-04-08 10:37:35 +00:00
syuilo
cd915b6939 [ci skip] Update CHANGELOG.md 2023-04-08 18:44:03 +09:00
syuilo
a7455035c5 Update CHANGELOG.md 2023-04-08 17:42:34 +09:00
atsuchan
5cd93834d1 enhance(client): 広告・お知らせが新規登録時に増殖しないように (#10412) 2023-04-08 17:42:08 +09:00
okayurisotto
6892e9fbb7 fix(frontend): URLに他のクエリがあるときにもZen UIが適用されるように (#10477) 2023-04-08 17:41:23 +09:00
syuilo
abe73c9904 New Crowdin updates (#10512)
* New translations ja-JP.yml (Chinese Simplified)

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

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Polish)

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

* 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 (Catalan)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Greek)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Swedish)

* New translations ja-JP.yml (Turkish)

* New translations ja-JP.yml (Ukrainian)

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

* 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 (Croatian)

* New translations ja-JP.yml (Uyghur)

* New translations ja-JP.yml (Lojban)

* New translations ja-JP.yml (Sinhala)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Haitian Creole)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Swedish)
2023-04-08 17:40:31 +09:00
syuilo
cc57fc041a Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-08 17:39:48 +09:00
syuilo
90df0d87ae disable flaky test 2023-04-08 17:39:44 +09:00
Acid Chicken (硫酸鶏)
95f670e3ec test: use M PLUS Rounded 1c on Storybook (#10520)
* test: use M PLUS Rounded 1c on Storybook

* test: fix hostname
2023-04-08 17:18:00 +09:00
omi
66342337ef chore: local test が redis version 起因で落ちる (#10519) 2023-04-08 17:06:42 +09:00
Acid Chicken (硫酸鶏)
59693bcb65 docs: thanks (#10518) 2023-04-08 17:02:18 +09:00
Acid Chicken (硫酸鶏)
a432f53d6c ci: avoid 0000000 2023-04-08 07:16:10 +00:00
syuilo
e72d6c9f2c 13.11.0-beta.8 2023-04-08 16:15:43 +09:00
Namekuji
e23cf77f86 fetch person again (#10514) 2023-04-08 16:13:50 +09:00
syuilo
0f328e8472 Update CHANGELOG.md 2023-04-08 16:09:43 +09:00
syuilo
7636536973 tweak locale 2023-04-08 16:06:08 +09:00
syuilo
975e79fa3b fix type 2023-04-08 15:59:41 +09:00
syuilo
eefa06622b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-08 15:53:39 +09:00
syuilo
9114c8cb8e feat(backend): support replication of postgresql
Resolve #10205
2023-04-08 15:53:36 +09:00
syuilo
132feea1fa :art 2023-04-08 15:49:29 +09:00
Acid Chicken (硫酸鶏)
33e9428510 chore: disable sampling for cat ears (#10513)
* chore: sample 3px for cat ears

* fix: typo

* chore: disable avatar plot for cat ears
2023-04-08 06:22:39 +00:00
syuilo
1aa8f09b95 13.11.0-beta.7 2023-04-08 14:42:11 +09:00
syuilo
65584f21bb Update misskey-js.api.md 2023-04-08 14:39:54 +09:00
syuilo
b697f948df Update misskey-js.api.md 2023-04-08 14:32:52 +09:00
syuilo
551b00302b use css module 2023-04-08 14:32:21 +09:00
syuilo
05aa0fa11a 🎨 2023-04-08 14:31:09 +09:00
syuilo
008e3fb37e Create 1680931179228-account-move.js 2023-04-08 14:20:40 +09:00
syuilo
68e8892f61 chore: use for instead of forEach 2023-04-08 14:18:28 +09:00
Namekuji
25ebb73756 feat: account migration (#10507)
* add Move activity

* add endpoint to move from local to remote

* follow move activity coming to inbox

* fix move endpoint

* add known-as endpoint to create account alias

* add migration page

* add route to migration page

* add move and known-as endpoints

* fix dependnecies error

* fix new endpoints

* fix move activity id

* fix refollow

* add movedToUri and alsoKnownAs to api

* fix moveToUri indicator

* fix missing context

* add chengelog

* rename MkMoved to MkAccountMoved

* add missing semicolon

* fix targetUri

* fix followings query

* remove redundant null check
2023-04-08 14:16:26 +09:00
syuilo
fa67fb42b1 enhance(backend): Redisにチャンネル投稿がない場合はDBから持ってくるように
Fix #10473
2023-04-08 13:12:49 +09:00
syuilo
40b3041608 tweak MkWindow.vue 2023-04-08 13:01:28 +09:00
syuilo
e3b8e8746e lint fixes 2023-04-08 12:57:05 +09:00
syuilo
33bce49ee8 lint fix 2023-04-08 12:56:21 +09:00
remi
44a4faebc0 feat: add minimize/Fold button for windows (#10508)
* add window minimizing!

* Fix window being able to go offscreen

* Revert en-US.yml changes

---------

Co-authored-by: mothmoon <remilia@remilia.se>
2023-04-08 12:55:05 +09:00
Acid Chicken (硫酸鶏)
79f198e4f1 ci: ignore develop commits 2023-04-07 23:27:22 +09:00
syuilo
eb30976ae6 Update CHANGELOG.md 2023-04-07 20:35:58 +09:00
Acid Chicken (硫酸鶏)
abda3b6c8c ci(#10336): use TurboSnap (#10494)
* ci(#10336): use TurboSnap

* build: fix version

* ci(#10336): update build dir

* chore(#10336): fire changes

* chore: stabilize icon width on `PageHeader`

* chore: fire changes

* ci: invalid ignore

* ci: trace logs

* chore: debug

* revert: debug

This reverts commit 2329165e25.

* chore: do not reuse build dir

* build: scripts

* ci: tweak

* revert: re-revert debug

This reverts commit 596ef05d9e.

* chore: detect changes manually

* fix: syntax

* ci: do not use only-changed

* ci: fix command

* revert: re-re-revert debug

This reverts commit b027170d75.

* ci: use build dir

* revert: re-re-re-revert debug

This reverts commit 529ab126ed.

* ci: fix path

* revert: re-re-re-re-revert debug

This reverts commit 0b0c0b9ea4.

* ci: fix typo

* ci: only show story files

* revert: re-re-re-re-re-revert debug

This reverts commit 9f5b88df32.

* ci: skip when no stories found

* ci: use skip

* revert: re-re-re-re-re-re-revert debug

This reverts commit 0df4bdc30b.

* ci: fix micromatch version

* revert: re-re-re-re-re-re-re-revert debug

This reverts commit 63063b02bb.

* revert: re-re-re-re-re-re-re-re-revert debug

This reverts commit 01d9669e2a.

* chore: pin tabler icon width globally

* ci: notify when Chromatic skips

* ci: fix endpoint

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-04-07 20:34:23 +09:00
syuilo
191ed3c814 fix(test): disable remote user tests
#10495
2023-04-07 20:09:22 +09:00
syuilo
7da2ca1862 Revert "test"
This reverts commit cb39db100a.
2023-04-07 20:05:15 +09:00
syuilo
cb39db100a test 2023-04-07 19:45:08 +09:00
syuilo
e3f4c9bcf6 13.11.0-beta.6 2023-04-07 18:57:18 +09:00
syuilo
ed4a100e96 fix(backend): restore date properly of federated instance cache 2023-04-07 18:55:11 +09:00
syuilo
1377ea4178 perf(backend): improve cache of federated instances 2023-04-07 18:48:45 +09:00
syuilo
6e1ae7b242 enhance(backend): tweak cache of role
#10439
2023-04-07 18:15:47 +09:00
syuilo
930724f9de Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-07 18:12:27 +09:00
syuilo
3bbeec70ec perf(backend): improve cache of instance of user 2023-04-07 18:12:24 +09:00
okayurisotto
69828e8dac fix(sw): プッシュ通知の操作でZen UIなクライアントが開かれてしまう場合がある問題を修正 (#10497) (#10498) 2023-04-07 15:48:49 +09:00
syuilo
58c8d21348 13.11.0-beta.5 2023-04-07 12:01:52 +09:00
syuilo
1e3fb5531b typo 2023-04-07 11:33:15 +09:00
syuilo
239d3f2dbf feat(backend): ジョブキュー用Redisを別サーバーに分離できるように 2023-04-07 11:27:01 +09:00
syuilo
ff6d9d2860 feat(backend): イベント用Redisを別サーバーに分離できるように 2023-04-07 11:20:14 +09:00
syuilo
f4588f3907 perf(backend): reduce db query 2023-04-07 10:07:41 +09:00
syuilo
a561b83070 fix deps 2023-04-06 20:29:17 +09:00
syuilo
88f22da052 update deps 2023-04-06 20:22:26 +09:00
syuilo
89c12948a1 13.11.0-beta.4 2023-04-06 19:51:33 +09:00
syuilo
f8cdab6d02 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-06 19:48:28 +09:00
syuilo
521eb95dbc perf(backend): cache avatar and banner url to db 2023-04-06 19:48:24 +09:00
syuilo
c29f21ac56 perf(backend): prevent emit needless db queries 2023-04-06 19:45:12 +09:00
syuilo
de9d136a3f perf(backend): reduce db query 2023-04-06 18:27:42 +09:00
syuilo
a0d5e2469d Update about-misskey.vue 2023-04-06 18:08:35 +09:00
syuilo
b38811af7c fix(backend): fix pack of notification behaviour 2023-04-06 15:09:21 +09:00
tamaina
bc5be83e29 use pnpm@8.1.1 2023-04-06 01:56:58 +00:00
syuilo
52011610c6 13.11.0.beta-3 2023-04-06 11:45:47 +09:00
syuilo
a2e022631d New Crowdin updates (#10430)
* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

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

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

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

* New translations ja-JP.yml (Italian)

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Russian)

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (Chinese Simplified)
2023-04-06 11:45:07 +09:00
syuilo
a574d0fbcb fix 2023-04-06 11:36:20 +09:00
syuilo
e9165a0d95 13.11.0.beta-2 2023-04-06 11:32:14 +09:00
syuilo
73203a3d72 perf(backend): cache local custom emojis 2023-04-06 11:14:43 +09:00
syuilo
437de6417e Update CHANGELOG.md 2023-04-06 10:30:56 +09:00
syuilo
fd05826187 fix(ci): use redis 7 2023-04-06 10:30:06 +09:00
syuilo
bd142b8fe5 refactor 2023-04-06 10:12:30 +09:00
syuilo
384a3bed09 perf(backend): reduce db query 2023-04-06 10:09:44 +09:00
syuilo
f54a9542bb Merge pull request #10402 from misskey-dev/develop
Release: 13.10.3
2023-03-25 08:36:41 +09:00
syuilo
a52bbc7c8d Merge pull request #10388 from misskey-dev/develop
Release: 13.10.2
2023-03-22 18:47:10 +09:00
syuilo
59768bdf3f Merge pull request #10383 from misskey-dev/develop
Release: 13.10.1
2023-03-22 16:30:36 +09:00
syuilo
1e67e9c661 Merge pull request #10342 from misskey-dev/develop
Release: 13.10.0
2023-03-22 09:55:38 +09:00
syuilo
ae517a99a7 Merge pull request #10218 from misskey-dev/develop
Release: 13.9.2
2023-03-06 11:54:12 +09:00
syuilo
b23a9b1a88 Merge pull request #10181 from misskey-dev/develop
Release: 13.9.1
2023-03-03 20:56:50 +09:00
syuilo
5bd68aa3e0 Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
2023-03-03 15:35:40 +09:00
syuilo
647ce174b3 Merge pull request #10112 from misskey-dev/develop
Release: 13.8.1
2023-02-26 20:57:13 +09:00
syuilo
02c8fd9de5 Merge pull request #10108 from misskey-dev/develop
* Add dialog to remove follower (#9718)

* update PULL_REQUEST_TEMPLATE

* 起動時にRedisの疎通確認を行う (#9832)

* 起動時にRedisの疎通確認を行う

* check:connectをstart内に移動

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* Pass `--detectOpenHandles` to Jest (#9895)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* enhance(client): MkUrlPreviewの閉じるボタンを見やすく (#9913)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* test(backend): restore ap-request tests (#9997)

Co-authored-by: tamaina <tamaina@hotmail.co.jp>

* fix/refaftor(client): MkTime.vueの変更 (#10061)

* fix(client): MkTime.timeにstringでもDateでない値が入った場合、?を表示

* fix(client): MkTimeを改良

* numberを許容

* falsyな値もとる

* 不明

* ありません

* fix

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする (#9911)

* fix(server): notes/createで、fileIdsと見つかったファイルの数が異なる場合はエラーにする

* NO_SUCH_FILE

* Update codecov.yml

* Update apple-touch-icon.png

* デプロイされているプレビュー環境がない場合はプレビュー環境を削除しないようにする (#10062)

* デプロイされているプレビュー環境がない場合はDestroy preview environmentを実行しないようにする

* CIがない場合の処理追加

* enhance(client): improve clip menu ux

* 未知のユーザーが deleteActor されたら処理をスキップする (#10067)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正 (#10069)

* fix(client): Android ChromeでPWAとしてインストールできない問題を修正

* 順番関係ある?

* Windows環境でswcを使うと正常にビルドができない問題の修正 (#10074)

* Update @swc/core to v1.3.36

* Update CHANGELOG.md

* Update CHANGELOG.md

* バックグラウンドで一定時間経過したらページネーションのアイテム更新をしない (#10053)

* 🎨

* feat: 2つの検索画面の統合 (#9949) (#10038)

* feat: 検索画面の UI を統一

* fix: エラーの修正

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): ノートメニューからユーザーメニューを開けるように

Resolve #10019

* enhance(client): renoteした際の表示を改善

Resolve #10078

* Update CHANGELOG.md

* enhance(client): tweak contextmenu position calculation

* 🎨

* 🎨

* feat: in-channel featured note

Resolve #9938

* refactor(frontend): fix eslint error (#10084)

* Simplify search.vue (remove dead code) (#10088)

* Simplify search.vue

This is already handled by the code above it, no need to handle it twice

* Remove unused imports

* Update about-misskey.vue

* test(server): add validation test of api:notes/create (#10090)

* fix(server): notes/createのバリデーションが効いていない
Fix #10079

Co-Authored-By: mei23 <m@m544.net>

* anyOf内にバリデーションを書いても最初の一つしかチェックされない

* ✌️

* wip

* wip

* ✌️

* RequiredProp

* Revert "RequiredProp"

This reverts commit 7469390011.

* add api:notes/create

* fix lint

* text

* ✌️

* improve readability

---------

Co-authored-by: mei23 <m@m544.net>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* New Crowdin updates (#10059)

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

* 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)

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Ukrainian)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

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

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

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* enhance(client): improve user menu ux

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように (#10098)

* enhance(client): photoswipe 表示時に戻る操作をしても前の画面に戻らないように

* add: changelog

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* enhance(client): メニューの「もっと」からインスタンス情報を見れるように

* [Fix] fixed an typo in error message (#10102)

* Update codecov.yml

* Update CHANGELOG.md

* fix(server): エラーのスタックトレースは返さないように

Fix #10064

* [chore]Editorconfig: ymlに加えてyamlファイルに対しても同じ規約を適用する (#10081)

* Added yaml file in addition to yml file, in editorconfig

* Applied editorconfig for pnpm-workspace.yaml

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* update deps

* ホームタイムラインの読み込みでクエリタイムアウトになるのを修正する (#10106)

* refactor

* New translations ja-JP.yml (French) (#10103)

* Update CHANGELOG.md

* 13.8.0

---------

Co-authored-by: atsuchan <83960488+atsu1125@users.noreply.github.com>
Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Co-authored-by: xianon <xianon@hotmail.co.jp>
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
Co-authored-by: YS <47836716+yszkst@users.noreply.github.com>
Co-authored-by: Khsmty <me@khsmty.com>
Co-authored-by: Soni L <EnderMoneyMod@gmail.com>
Co-authored-by: mei23 <m@m544.net>
Co-authored-by: daima3629 <52790780+daima3629@users.noreply.github.com>
Co-authored-by: Windymelt <1113940+windymelt@users.noreply.github.com>
2023-02-26 20:21:54 +09:00
syuilo
1ba49b614d Merge pull request #10058 from misskey-dev/develop
Release: 13.7.5
2023-02-24 13:06:55 +09:00
tamaina
40de14415c Release: 13.7.4
Merge pull request #10050 from misskey-dev/develop
2023-02-23 23:11:25 +09:00
tamaina
7c9330a02f Release: 13.7.3
Merge pull request #10048 from misskey-dev/develop
2023-02-23 22:15:56 +09:00
255 changed files with 6206 additions and 2679 deletions

View File

@@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@@ -51,6 +51,23 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
@@ -62,6 +79,22 @@ redis:
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@@ -2,10 +2,6 @@ name: Storybook
on:
push:
branches:
- master
- develop
pull_request:
branches-ignore:
- l10n_develop
@@ -13,6 +9,9 @@ jobs:
build:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v3.3.0
with:
@@ -36,21 +35,38 @@ jobs:
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
env:
NODE_OPTIONS: "--max_old_space_size=7168"
- name: Publish to Chromatic
id: chromatic
uses: chromaui/action@v1
with:
exitOnceUploaded: true
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
storybookBuildDir: storybook-static
workingDir: packages/frontend
- name: Compare on Chromatic
if: github.event_name == 'pull_request_target'
run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
if: github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.ref != 'refs/heads/master'
id: chromatic
run: |
DIFF="${{ github.event.before }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic will skip testing
uses: actions/github-script@v6.4.0
if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/develop' && steps.chromatic.outputs.skip == 'true'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:

View File

@@ -24,7 +24,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
image: redis:7
ports:
- 56312:6379

View File

@@ -63,7 +63,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:6
image: redis:7
ports:
- 56312:6379

View File

@@ -5,33 +5,115 @@
-
### Client
-
-
### Server
-
-->
## 13.x.x (unreleased)
## 13.11.3
### General
- 指定したロールを持つユーザーのノートのみが流れるロールタイムラインを追加
- Deckのカラムとしても追加可能
- カスタム絵文字関連の改善
* ートなどに含まれるemojispopulateEmojiの結果プロキシされたURLではなくオリジナルのURLを指すように
* MFMでx3/x4もしくはscale.x/yが2.5以上に指定されていた場合にはオリジナル品質の絵文字を使用するように
- カスタム絵文字でリアクションできないことがある問題を修正
### Client
- チャンネルのピン留めされたノートの順番が正しくない問題を修正
### Server
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
- Misskey Webでのサーバーサイドエラー画面を改善
- Misskey Webでのサーバーサイドエラーのログが残るように
- ノート作成時のアンテナ追加パフォーマンスを改善
- アンテナとロールTLのuntil/sinceプロパティが動くように
## 13.11.2
### Note
- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照)
### General
- チャンネルの検索用ページの追加
### Client
- 常に広告を見られるオプションを追加
- ユーザーページの画像一覧が表示されない問題を修正
- webhook, 連携アプリ一覧でコンテンツが重複して表示される問題を修正
- iPhoneで絵文字ピッカーの表示が崩れる問題を修正
- iPhoneでウィジェットドロワーの「ウィジェットを編集」が押しにくい問題を修正
- 投稿フォームのデザインを調整
- ギャラリーの人気の投稿が無限にページングされる問題を修正
### Server
- channels/search Endpoint APIの追加
- APIパラメータサイズ上限を32kbから1mbに緩和
- プッシュ通知送信時のパフォーマンスを改善
- ローカルのカスタム絵文字のキャッシュが効いていなかった問題を修正
- アンテナのノート、チャンネルのノート、通知が正常に作成できないことがある問題を修正
- ストリーミングのLTLチャンネルでサーバー側にエラーログが出るのを修正
### Service Worker
- 「通知が既読になったらプッシュ通知を削除する」を復活
* 「プッシュ通知が更新されました」の挙動を変えた(ホストとバージョンを表示するようにし、一定時間後の削除は行わないように)
- プッシュ通知が実績を解除 (achievementEarned) に対応
- プッシュ通知のアクションから既存のクライアントの投稿フォームを開くことになった際の挙動を修正
- たくさんのプッシュ通知を閉じた際、その通知の数だけnotifications/mark-all-as-readを叩くのをやめるように
## 13.11.1
### General
- チャンネルの投稿を過去までさかのぼれるように
### Client
- PWA時の絵文字ピッカーの位置をホームバーに重ならないように調整
- リスト管理の画面でリストが無限に読み込まれる問題を修正
- 自分のクリップが無限に読み込まれる問題を修正
- チャンネルのお気に入りが無限に読み込まれる問題を修正
- さがすのローカルユーザー(ピンどめ)が無限に生成される問題を修正
- チャンネルを新規作成できない問題を修正
- ユーザープレビューが表示されない問題を修正
### Server
- 通知読み込みでエラーが発生する場合がある問題を修正
- リアクションできないことがある問題を修正
- IDをaid以外に設定している場合の問題を修正
- 連合しているインスタンスについて予期せず配送が全て停止されることがある問題を修正
## 13.11.0
### NOTE
- このバージョンからRedis 7.xが必要です。
- アップデートを行うと全ての通知およびアンテナのノートはリセットされます。
### General
- チャンネルをお気に入りに登録できるように
- タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。
- チャンネルにノートをピン留めできるように
### Client
- 投稿フォームのデザインを改善
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
- オブジェクトストレージの設定画面を分かりやすく
- 広告・お知らせが新規登録時に増殖しないように
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
- Add Minimizing ("folding") of windows
### Server
- PostgreSQLのレプリケーション対応
- 設定ファイルの `dbReplications` および `dbSlaves` にて設定できます
- イベント用Redisを別サーバーに分離できるように
- ジョブキュー用Redisを別サーバーに分離できるように
- サーバーの全体的なパフォーマンスを向上
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上

View File

@@ -61,10 +61,18 @@ Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), s
## Thanks
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" width="117" height="30" alt="Docker" /></a>
<a href="https://about.codecov.io/for/open-source/"><img src="https://about.codecov.io/wp-content/themes/codecov/assets/brand/sentry-cobranding/logos/codecov-by-sentry-logo.svg" height="30" alt="Codecov" /></a>
Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage.
<a href="https://crowdin.com/"><img src="https://user-images.githubusercontent.com/20679825/230709597-1299a011-171a-4294-a91e-355a9b37c672.svg" height="30" alt="Crowdin" /></a>
Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages.
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a>
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.

View File

@@ -72,16 +72,50 @@ db:
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────

View File

@@ -45,7 +45,7 @@ gulp.task('build:backend:script', () => {
});
gulp.task('build:backend:style', () => {
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
.pipe(cssnano({
zindex: false
}))

View File

@@ -1277,4 +1277,3 @@ _deck:
_webhookSettings:
name: "الإسم"
active: "مفعّل"

View File

@@ -1356,4 +1356,3 @@ _deck:
_webhookSettings:
name: "নাম"
active: "চালু"

View File

@@ -460,4 +460,3 @@ _deck:
list: "Llistes"
mentions: "Mencions"
direct: "Publicacions directes"

View File

@@ -779,4 +779,3 @@ _deck:
_webhookSettings:
name: "Jméno"
active: "Zapnuto"

View File

@@ -1,3 +1,2 @@
---
_lang_: "Dansk"

View File

@@ -20,6 +20,7 @@ noNotes: "Keine Notizen gefunden"
noNotifications: "Keine Benachrichtigungen gefunden"
instance: "Instanz"
settings: "Einstellungen"
notificationSettings: "Benachrichtigungseinstellungen"
basicSettings: "Allgemeine Einstellungen"
otherSettings: "Weitere Einstellungen"
openInWindow: "In einem Fenster öffnen"
@@ -67,7 +68,7 @@ import: "Import"
export: "Export"
files: "Dateien"
download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Notizen mit dieser Datei werden ebenso verschwinden."
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, auch verschwinden."
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
@@ -196,7 +197,7 @@ instanceInfo: "Instanzinformationen"
statistics: "Statistiken"
clearQueue: "Warteschlange leeren"
clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich leeren?"
clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht förderiert. Normalerweise wird dies nicht benötigt."
clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht föderiert. Normalerweise wird dies nicht benötigt."
clearCachedFiles: "Cache leeren"
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
blockedInstances: "Blockierte Instanzen"
@@ -500,12 +501,13 @@ objectStoragePrefixDesc: "Dateien werden in Ordnern unter diesem Prefix gespeich
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten Endpoint im Format „<host>“ oder „<host>:<port>“ angeben."
objectStorageRegion: "Region"
objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, lass dieses Feld leer oder gib „us-east-1“ an."
objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, gib „us-east-1“ an. Lasse es leer bei Verwendung von AWS Konfigurationsdateien oder Umgebungsvariablen."
objectStorageUseSSL: "SSL verwenden"
objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein HTTPS verwenden wirst"
objectStorageUseProxy: "Über Proxy verbinden"
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
s3ForcePathStyleDesc: "Ist s3ForcePathStyle aktiviert, so muss der Bucketname nicht im Hostnamen der URL, sondern im Pfad der URL angeben werden. Diese Option muss eventuell aktiviert werden, wenn Dienste wie z.B. eine selbstbetriebene Minio-Instanz verwendet werden."
serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen"
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
@@ -919,6 +921,7 @@ pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterst
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald die relevanten Benachrichtigungen oder Nachrichten gelesen wurden"
sendPushNotificationReadMessageCaption: "Eine Push-Benachrichtigungen mit dem Inhalt \"{emptyPushNotificationMessage}\" wird kurz eingeblendet. Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
windowMaximize: "Maximieren"
windowMinimize: "Minimieren"
windowRestore: "Wiederherstellen"
caption: "Beschreibung"
loggedInAsBot: "Momentan als Bot angemeldet"
@@ -960,7 +963,9 @@ copyErrorInfo: "Fehlerdetails kopieren"
joinThisServer: "Bei dieser Instanz registrieren"
exploreOtherServers: "Eine andere Instanz finden"
letsLookAtTimeline: "Die Chronik durchstöbern"
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt."
disableFederationConfirm: "Föderation wirklich deaktivieren?"
disableFederationConfirmWarn: "Auch mit deaktivierter Föderation bleiben Notizen, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird dies nicht benötigt."
disableFederationOk: "Deaktivieren"
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
postToTheChannel: "In Kanal senden"
@@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverl
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
largeNoteReactions: "Reaktionen vergrößert anzeigen"
noteIdOrUrl: "Notiz-ID oder URL"
accountMigration: "Konto-Umzug"
accountMoved: "Dieser Benutzer ist zu einem neuen Konto umgezogen:"
forceShowAds: "Werbung immer anzeigen"
_accountMigration:
moveTo: "Dieses Konto zu einem neuen umziehen"
moveToLabel: "Umzugsziel:"
moveAccountDescription: "Ein Umzug kann nicht rückgängig gemacht werden. Stelle zuerst sicher, dass du auf dem Umzugsziel einen Alias erstellt hast. Gib dann das Umzugsziel in folgendem Format ein: @person@instance.com"
moveFrom: "Von einem anderen Konto zu diesem umziehen"
moveFromLabel: "Umzugsursprung:"
moveFromDescription: "Stelle sicher, dass du auf dem Umzugsursprungskonto einen Alias zu diesem Konto erstellt hast, falls du die Follower des Ursprungskontos übertragen möchtest. Dies muss vor dem Umzug geschehen! Gib dann das Ursprungskonto in folgendem Format an: @person@instance.com"
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden.\n\nÜberprüfe zusätzlich, dass du auf dem Umzugsziel einen Alias eingerichtet hast."
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Kann Einladungscodes für diese Instanz erstellen"
canInvite: "Erstellung von Einladungscodes für diese Instanz"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen"
@@ -1390,6 +1408,8 @@ _channel:
following: "Gefolgt"
usersCount: "{n} Teilnehmer"
notesCount: "{n} Notizen"
nameAndDescription: "Name und Beschreibung"
nameOnly: "Nur Name"
_menuDisplay:
sideFull: "Seitlich"
sideIcon: "Seitlich (Icons)"
@@ -1408,7 +1428,7 @@ _instanceMute:
instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz."
instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben"
title: "Blendet Notizen von stummgeschalteten Instanzen aus."
heading: "Liste der stummzuschaltenden Instanzen"
heading: "Stummzuschaltende Instanzen"
_theme:
explore: "Farbschemata erforschen"
install: "Farbschemata installieren"
@@ -1680,7 +1700,7 @@ _visibility:
followersDescription: "Nur für Follower sichtbar"
specified: "Direkt"
specifiedDescription: "Nur für bestimmte Benutzer sichtbar"
disableFederation: "Deförderiert"
disableFederation: "Deföderieren"
disableFederationDescription: "Nicht an andere Instanzen übertragen"
_postForm:
replyPlaceholder: "Dieser Notiz antworten …"
@@ -1870,6 +1890,7 @@ _deck:
channel: "Kanal"
mentions: "Erwähnungen"
direct: "Direktnachrichten"
roleTimeline: "Rollenchronik"
_dialog:
charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}"
charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}"
@@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@@ -394,4 +394,3 @@ _deck:
mentions: "Επισημάνσεις"
_webhookSettings:
name: "Όνομα"

View File

@@ -20,6 +20,7 @@ noNotes: "No notes"
noNotifications: "No notifications"
instance: "Instance"
settings: "Settings"
notificationSettings: "Notification Settings"
basicSettings: "Basic Settings"
otherSettings: "Other Settings"
openInWindow: "Open in window"
@@ -67,7 +68,7 @@ import: "Import"
export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it."
unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while."
@@ -500,12 +501,13 @@ objectStoragePrefixDesc: "Files will be stored under directories with this prefi
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Leave this empty if you are using AWS S3, otherwise specify the endpoint as '<host>' or '<host>:<port>', depending on the service you are using."
objectStorageRegion: "Region"
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, leave this blank or enter 'us-east-1'."
objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, enter 'us-east-1'. Leave empty if using AWS configuration files or environment variables."
objectStorageUseSSL: "Use SSL"
objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for API connections"
objectStorageUseProxy: "Connect over Proxy"
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
objectStorageSetPublicRead: "Set \"public-read\" on upload"
s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance."
serverLogs: "Server logs"
deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline"
@@ -917,8 +919,9 @@ unsubscribePushNotification: "Disable push notifications"
pushNotificationAlreadySubscribed: "Push notifications are already enabled"
pushNotificationNotSupported: "Your browser or instance does not support push notifications"
sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read"
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable."
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the power consumption of your device."
windowMaximize: "Maximize"
windowMinimize: "Minimize"
windowRestore: "Restore"
caption: "Caption"
loggedInAsBot: "Currently logged in as bot"
@@ -960,7 +963,9 @@ copyErrorInfo: "Copy error details"
joinThisServer: "Sign up at this instance"
exploreOtherServers: "Look for another instance"
letsLookAtTimeline: "Have a look at the timeline"
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting."
disableFederationConfirm: "Really disable federation?"
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
disableFederationOk: "Disable"
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel"
@@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
largeNoteReactions: "Enlargen displayed reactions"
noteIdOrUrl: "Note ID or URL"
accountMigration: "Account Migration"
accountMoved: "This user has moved to a new account:"
forceShowAds: "Always show ads"
_accountMigration:
moveTo: "Migrate this account to a different one"
moveToLabel: "Account to move to:"
moveAccountDescription: "This action is irreversible. First, create an alias for this account on the account you wish to move to. Then, enter the account to move to in the following format: @person@instance.com"
moveFrom: "Migrate another account to this one"
moveFromLabel: "Account to move from:"
moveFromDescription: "Create an alias for the account to move from on this account if you wish to transfer its followers. This has to be done before the transfer! Then, enter the account to move to in the following format: @person@instance.com"
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore.\n\nAlso, confirm you've created an alias at the account to migrate to."
_achievements:
earnedAt: "Unlocked at"
_types:
@@ -1390,6 +1408,8 @@ _channel:
following: "Followed"
usersCount: "{n} Participants"
notesCount: "{n} Notes"
nameAndDescription: "Name and description"
nameOnly: "Name only"
_menuDisplay:
sideFull: "Side"
sideIcon: "Side (Icons)"
@@ -1680,7 +1700,7 @@ _visibility:
followersDescription: "Make visible to your followers only"
specified: "Direct"
specifiedDescription: "Make visible for specified users only"
disableFederation: "Unfederated"
disableFederation: "Defederate"
disableFederationDescription: "Don't transmit to other instances"
_postForm:
replyPlaceholder: "Reply to this note..."
@@ -1852,7 +1872,7 @@ _deck:
swapRight: "Swap with the right column"
swapUp: "Swap with the above column"
swapDown: "Swap with the below column"
stackLeft: "Stack with the left column"
stackLeft: "Stack on left column"
popRight: "Pop column to the right"
profile: "Profile"
newProfile: "New profile"
@@ -1870,6 +1890,7 @@ _deck:
channel: "Channel"
mentions: "Mentions"
direct: "Direct notes"
roleTimeline: "Role Timeline"
_dialog:
charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}."
charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}."
@@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"

View File

@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión
objectStorageUseProxy: "Conectarse a través de Proxy"
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos"
objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
s3ForcePathStyleDesc: "Si s3ForcePathStyle esta habilitado el nombre del bucket debe ser especificado como parte de la URL en lugar del nombre de host en la URL. Puede ser necesario activar esta opción cuando se utilice, por ejemplo, Minio en un servidor propio."
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
@@ -919,6 +920,7 @@ pushNotificationNotSupported: "El navegador o la instancia no admiten notificaci
sendPushNotificationReadMessage: "Eliminar las notificaciones push después de leer las notificaciones y los mensajes"
sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificationMessage}\" aparecerá momentáneamente. Esto puede aumentar el consumo de batería del dispositivo."
windowMaximize: "Maximizar"
windowMinimize: "Minimizar"
windowRestore: "Regresar"
caption: "Pie de foto"
loggedInAsBot: "Inicio sesión como cuenta bot."
@@ -960,7 +962,9 @@ copyErrorInfo: "Copiar detalles del error"
joinThisServer: "Registrarse en esta instancia"
exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local"
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración."
disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?"
disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar."
disableFederationOk: "Desactivar."
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
postToTheChannel: "Publicar en el canal"
@@ -980,6 +984,21 @@ drivecleaner: "Limpiador del Drive"
retryAllQueuesNow: "Reintentar inmediatamente todas las colas"
retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?"
retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente "
enableChartsForRemoteUser: "Generar gráficas de usuarios remotos."
enableChartsForFederatedInstances: "Generar gráficos de servidores remotos"
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
largeNoteReactions: "Agrandar las reacciones de las notas"
noteIdOrUrl: "ID o URL de la nota"
accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
_accountMigration:
moveTo: "Mover esta cuenta a una nueva"
moveToLabel: "Cuenta destino:"
moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com"
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com"
migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado."
_achievements:
earnedAt: "Desbloqueado el"
_types:
@@ -1276,6 +1295,8 @@ _role:
followersMoreThanOrEq: "Tiene X o más seguidores"
followingLessThanOrEq: "Sigue X o menos cuentas"
followingMoreThanOrEq: "Sigue X o más cuentas"
notesLessThanOrEq: "El número de notas es inferior o igual a"
notesMoreThanOrEq: "El número de notas es superior o igual a"
and: "Condicional AND"
or: "Condicional OR"
not: "Condicional NOT"
@@ -1875,6 +1896,16 @@ _drivecleaner:
orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos"
_webhookSettings:
createWebhook: "Crear Webhook"
name: "Nombre"
secret: "Secreto"
events: "Eventos de webhook"
active: "Activado"
_events:
follow: "Cuando se sigue a alguien"
followed: "Cuando se es seguido"
note: "Cuando se publica una nota"
reply: "Cuando se recibe una respuesta"
renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención"

View File

@@ -1470,4 +1470,3 @@ _deck:
_webhookSettings:
name: "Nom"
active: "Activé"

View File

@@ -1,2 +1 @@
---

View File

@@ -1,2 +1 @@
---

View File

@@ -122,6 +122,8 @@ unmarkAsSensitive: "Hapus tanda konten sensitif"
enterFileName: "Masukkan nama berkas"
mute: "Bisukan"
unmute: "Hapus bisukan"
renoteMute: "Matikan renote"
renoteUnmute: "Batal mematikan renote"
block: "Blokir"
unblock: "Buka blokir"
suspend: "Bekukan"
@@ -393,11 +395,15 @@ about: "Informasi"
aboutMisskey: "Tentang Misskey"
administrator: "Admin"
token: "Token"
totp: "Aplikasi autentikator"
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
moderator: "Moderator"
moderation: "Moderasi"
nUsersMentioned: "{n} pengguna disebut"
securityKeyAndPasskey: "Security key dan passkey"
securityKey: "Kunci keamanan"
lastUsed: "Terakhir digunakan"
lastUsedAt: "Penggunaan terakhir: {t}"
unregister: "Batalkan pendaftaran"
passwordLessLogin: "Setel login tanpa kata sandi"
resetPassword: "Atur ulang kata sandi"
@@ -844,6 +850,7 @@ tenMinutes: "10 Menit"
oneHour: "1 Jam"
oneDay: "1 Hari"
oneWeek: "1 Bulan"
oneMonth: "satu bulan"
reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
rateLimitExceeded: "Batas sudah terlampaui"
@@ -901,6 +908,7 @@ pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pember
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
windowMaximize: "Maksimalkan"
windowMinimize: "Minimalkan"
windowRestore: "Kembalikan"
caption: "Keterangan"
loggedInAsBot: "Sedang login sebagai bot"
@@ -939,6 +947,12 @@ collapseRenotes: "Tutup renote yang sudah kamu lihat"
internalServerError: "Kesalahan internal peladen"
internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga"
copyErrorInfo: "Salin detil galat"
joinThisServer: "Gabung server ini"
exploreOtherServers: "Cari server lain"
letsLookAtTimeline: "LIhat timeline"
disableFederationConfirm: "Matikan federasi?"
disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan."
disableFederationOk: "Matikan federasi"
_achievements:
earnedAt: "Terbuka pada"
_types:
@@ -1806,4 +1820,3 @@ _deck:
_webhookSettings:
name: "Nama"
active: "Aktif"

View File

@@ -20,6 +20,7 @@ noNotes: "Nessuna nota!"
noNotifications: "Nessuna notifica"
instance: "Istanza"
settings: "Impostazioni"
notificationSettings: "Preferenze di notifica"
basicSettings: "Impostazioni generali"
otherSettings: "Altre impostazioni"
openInWindow: "Apri in una finestra"
@@ -170,7 +171,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
host: "Server remoto"
selectUser: "Seleziona profilo"
recipient: "Destinatario"
annotation: "Descrizione"
annotation: "Annotazione preventiva"
federation: "Federazione"
instances: "Istanza"
registeredAt: "Registrato presso"
@@ -212,7 +213,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
done: "Fine"
processing: "In elaborazione"
preview: "Anteprima"
default: "Medio"
default: "Predefinito"
defaultValueIs: "Predefinito: {value}"
noCustomEmojis: "Nessun emoji"
noJobs: "Nessun lavoro"
@@ -237,14 +238,14 @@ more: "Di più!"
featured: "Tendenze"
usernameOrUserId: "Nome utente o ID utente"
noSuchUser: "Nessun utente trovato"
lookup: "Cerca"
lookup: "Ricerca remota"
announcements: "Annunci"
imageUrl: "URL dell'immagine"
remove: "Elimina"
removed: "Eliminato con successo"
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?"
resetAreYouSure: "Reimposta"
resetAreYouSure: "Ripristinare?"
saved: "Salvato"
messaging: "Messaggi"
upload: "Carica"
@@ -409,7 +410,7 @@ lastUsedAt: "Uso più recente: {t}"
unregister: "Annulla l'iscrizione"
passwordLessLogin: "Accedi senza password"
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
resetPassword: "Reimposta password"
resetPassword: "Ripristina la password"
newPasswordIs: "La tua nuova password è「{password}」"
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
share: "Condividi"
@@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "Disabilita quest'opzione se non utilizzi HTTPS per le
objectStorageUseProxy: "Usa proxy"
objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API."
objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di caricare"
s3ForcePathStyleDesc: "L'attivazione di s3ForcePathStyle impone di specificare il nome del bucket come parte del percorso nell'URL anziché del nome host. Potrebbe tornare utile quando si utilizzano applicazioni come Minio."
serverLogs: "Log del server"
deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
@@ -564,7 +566,7 @@ invisibleNote: "Nota invisibile"
enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità"
poll: "Sondaggio"
useCw: "Nascondere media"
useCw: "Content Warning"
enablePlayer: "Visualizza"
disablePlayer: "Chiudi"
expandTweet: "Espandi tweet"
@@ -579,7 +581,7 @@ plugins: "Estensioni"
preferencesBackups: "Backup delle impostazioni"
deck: "Deck"
undeck: "Esci dal deck"
useBlurEffectForModal: "Utilizza effetto sfocatura per i modali"
useBlurEffectForModal: "Utilizza effetto sfocatura per le finestre modali"
useFullReactionPicker: "Usa la totalità del pannello di reazioni"
width: "Larghezza"
height: "Altezza"
@@ -596,7 +598,7 @@ notificationType: "Tipo di notifiche"
edit: "Modifica"
emailServer: "Server email"
enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per ripristinare la password"
email: "Email"
emailAddress: "Indirizzo di posta elettronica"
smtpConfig: "Impostazioni del server SMTP"
@@ -785,7 +787,7 @@ gallery: "Galleria"
recentPosts: "Le più recenti"
popularPosts: "Le più visualizzate"
shareWithNote: "Condividere in nota"
ads: "Pubblicità"
ads: "Banner"
expiration: "Scadenza"
startingperiod: "Periodo di inizio"
memo: "Promemoria"
@@ -814,7 +816,7 @@ translatedFrom: "Tradotto da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
aiChanMode: "Modalità Ai"
keepCw: "Mantieni il CW"
keepCw: "Mantieni il Content Warning"
pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
@@ -919,6 +921,7 @@ pushNotificationNotSupported: "Il client o il server non supporta le notifiche p
sendPushNotificationReadMessage: "Elimina le notifiche push dopo la relativa lettura"
sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria."
windowMaximize: "Ingrandisci"
windowMinimize: "Contrai finestra"
windowRestore: "Ripristina"
caption: "Didascalia"
loggedInAsBot: "Connessione come Bot"
@@ -960,16 +963,18 @@ copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline"
disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione."
invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
disableFederationConfirm: "Vuoi davvero disattivare la federazione?"
disableFederationConfirmWarn: "Anche se defederate, le Note continueranno ad essere pubbliche, se non diversamente specificato. Di solito, non è necessario far questo."
disableFederationOk: "Disabilita federazione"
invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale"
postToTheChannel: "Pubblica nel canale"
cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi reimpostare la password?"
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole sensibili"
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
@@ -980,6 +985,22 @@ drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
largeNoteReactions: "Ingrandisci le reazioni"
noteIdOrUrl: "ID della Nota o URL"
accountMigration: "Migrazione del profilo"
accountMoved: "Questo profilo ha migrato altrove:"
forceShowAds: "Mostra sempre i banner"
_accountMigration:
moveTo: "Migrare questo profilo verso un un altro"
moveToLabel: "Profilo verso cui migrare"
moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it"
moveFrom: "Migra un altro profilo dentro a questo"
moveFromLabel: "Profilo da cui migrare:"
moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
_achievements:
earnedAt: "Data di conseguimento"
_types:
@@ -1276,6 +1297,8 @@ _role:
followersMoreThanOrEq: "Ha più di N follower"
followingLessThanOrEq: "Segue N profili o meno"
followingMoreThanOrEq: "Segue N profili o più"
notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
notesMoreThanOrEq: "Conteggio Note maggiore o uguale a"
and: "E"
or: "O"
not: "NON"
@@ -1314,8 +1337,8 @@ _ad:
hide: "Nascondi"
_forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se nessun indirizzo e-mail è stato registrato, si prega di contattare l'amministratore·trice dell'istanza."
contactAdmin: "Poiché questa istanza non permette l'utilizzo di una mail, si prega di contattare l'amministratore·trice dell'istanza per poter ripristinare la password."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
_gallery:
my: "Le mie pubblicazioni"
liked: "Pubblicazioni che mi piacciono"
@@ -1385,6 +1408,8 @@ _channel:
following: "Seguiti"
usersCount: "{n} partecipanti"
notesCount: "{n} note"
nameAndDescription: "Nome e descrizione"
nameOnly: "Solo il nome"
_menuDisplay:
sideFull: "Laterale"
sideIcon: "Laterale (solo icone)"
@@ -1670,12 +1695,12 @@ _visibility:
public: "Pubblica"
publicDescription: "Visibile per tutti sul Fediverso"
home: "Home"
homeDescription: "Visibile solo sulla timeline \"Home\""
homeDescription: "Visibile solo sulla timeline locale"
followers: "Follower"
followersDescription: "Visibile solo per i tuoi follower"
followersDescription: "Visibile solo ai tuoi follower"
specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Interrompi la federazione"
disableFederation: "Federazione disabilitata"
disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm:
replyPlaceholder: "Rispondi a questa nota..."
@@ -1870,11 +1895,21 @@ _dialog:
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline:
title: "Timeline disabilitata"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"
description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
_drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings:
createWebhook: "Creazione Webhook"
name: "Nome"
secret: "Segreto"
events: "Quando eseguire il Webhook"
active: "Attivo"
_events:
follow: "Quando segui un profilo"
followed: "Quando ti segue un profilo"
note: "Quando pubblichi una Nota"
reply: "Quando rispondono ad una Nota"
renote: "Quando la Nota è Rinotata"
reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano"

View File

@@ -20,6 +20,7 @@ noNotes: "ノートはありません"
noNotifications: "通知はありません"
instance: "サーバー"
settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く"
@@ -917,9 +918,10 @@ subscribePushNotification: "プッシュ通知を有効化"
unsubscribePushNotification: "プッシュ通知を停止する"
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
sendPushNotificationReadMessage: "通知が既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "端末の電池消費量が増加する可能性があります。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す"
caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中"
@@ -988,6 +990,18 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
accountMigration: "アカウントの引っ越し"
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
forceShowAds: "常に広告を表示する"
_accountMigration:
moveTo: "このアカウントを新しいアカウントに引っ越す"
moveToLabel: "引っ越し先のアカウント:"
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用できなくなります。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
_achievements:
earnedAt: "獲得日時"
@@ -1414,6 +1428,8 @@ _channel:
following: "フォロー中"
usersCount: "{n}人が参加中"
notesCount: "{n}投稿があります"
nameAndDescription: "名前と説明"
nameOnly: "名前のみ"
_menuDisplay:
sideFull: "横"
@@ -1927,6 +1943,7 @@ _deck:
channel: "チャンネル"
mentions: "あなた宛て"
direct: "ダイレクト"
roleTimeline: "ロールタイムライン"
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@@ -1954,4 +1971,3 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"

View File

@@ -16,22 +16,23 @@ cancel: "やめとく"
noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenoteしたで"
noNotes: "ノートなんてあらへん"
noNotifications: "通知なんてあらへん"
noNotes: "ノートあらへん"
noNotifications: "通知あらへん"
instance: "サーバー"
settings: "設定"
notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介食ってもた"
noAccountDescription: "自己紹介はあらへん"
login: "ログイン"
loggingIn: "ログインしよるで"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしとるで"
save: "保存"
save: "とっとく"
users: "ユーザー"
addUser: "ユーザーを追加や"
favorite: "お気に入り"
@@ -81,9 +82,9 @@ followsYou: "フォローされとるで"
createList: "リスト作る"
manageLists: "リストの管理"
error: "エラー"
somethingHappened: "なんかアカンことが起こったで"
somethingHappened: "なんかあかんわ"
retry: "もっぺんやる?"
pageLoadError: "ページ読み込みに失敗してもうたわ"
pageLoadError: "ページ読み込めんかったわ"
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
@@ -108,8 +109,8 @@ inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく"
you: "あんた"
clickToShow: "押したら見えるで"
sensitive: "ちょっとアカンやつやで"
clickToShow: "押したら出ら"
sensitive: "気いつけて見いや"
add: "増やす"
reaction: "リアクション"
reactions: "リアクション"
@@ -122,8 +123,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん"
renoteUnmute: "リノートもやっぱ見るわ"
renoteMute: "Renoteは見いひん"
renoteUnmute: "Renoteもやっぱ見るわ"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
@@ -141,14 +142,14 @@ editWidgetsExit: "編集終ったで"
customEmojis: "カスタム絵文字"
emoji: "絵文字"
emojis: "絵文字"
emojiName: "絵文字"
emojiName: "絵文字はんの名前"
emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせ直リンクするようになるで。サーバーの容量は節約できるけど、サムネイル作られんくなるから通信量が増えるで。"
cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイル作らなるから通信量が増えるで。"
flagAsBot: "Botにするで"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsCat: "Catやで"
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
@@ -194,10 +195,10 @@ network: "ネットワーク"
disk: "ディスク"
instanceInfo: "サーバー情報"
statistics: "統計"
clearQueue: "キューにさいなら"
clearQueueConfirmTitle: "キューをクリアしまっか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら"
clearQueue: "キューをほかす"
clearQueueConfirmTitle: "キューをほかしとこか?"
clearQueueConfirmText: "未配達の投稿は配送されなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュをほかす"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
@@ -206,7 +207,7 @@ mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートを削除しまっか?"
noteDeleteConfirm: "このノートをほかしてええか?"
pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了したで管理者アカウントを作ってや。"
done: "でけた"
@@ -226,9 +227,9 @@ notResponding: "応答してへんで"
instanceFollowing: "サーバーのフォロー"
instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "サーバーのユーザー"
changePassword: "パスワード変える"
changePassword: "パスワードをいじる"
security: "セキュリティ"
retypedNotMatch: "入れたやつ同じになってないで。"
retypedNotMatch: "入れたやつ合うてへんわ。"
currentPassword: "今のパスワード"
newPassword: "次のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
@@ -258,7 +259,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
explore: "みつける"
messageRead: "もう読んだ"
noMoreHistory: "これより過去の履歴はあらへんで"
noMoreHistory: "これより昔のんはあらへんで"
startMessaging: "チャットやるで"
nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで"
@@ -294,14 +295,14 @@ createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加"
emptyDrive: "ドライブにはなんも残っとらん"
emptyDrive: "ドライブは空っぽや"
emptyFolder: "このフォルダーは空や"
unableToDelete: "消そうおもってんけどな、あかんかったわ"
unableToDelete: "消んかったわ"
inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
copyUrl: "URLをコピー"
rename: "名前を変えるで"
avatar: "アイコン"
@@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
serverLogs: "サーバーログ"
deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
@@ -643,7 +645,7 @@ reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
send: "送信"
abuseMarkAsResolved: "対応したで"
openInNewTab: "新しいタブで開く"
@@ -739,7 +741,7 @@ capacity: "容量"
inUse: "使用中"
editCode: "コードを編集"
apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
emailNotification: "メール通知"
publish: "公開"
inChannelSearch: "チャンネル内検索"
@@ -767,7 +769,7 @@ active: "アクティブ"
offline: "オフライン"
notRecommended: "あんま推奨しやんで"
botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック"
instanceBlocking: "サーバーブロック"
selectAccount: "アカウントを選んでなー"
switchAccount: "アカウントを変えるで"
enabled: "有効"
@@ -851,8 +853,8 @@ themeColor: "テーマカラー"
size: "大きさ"
numberOfColumn: "列の数"
searchByGoogle: "探す"
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
mutePeriod: "ミュートする期間"
period: "期限"
@@ -866,7 +868,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで"
cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしたってええか?"
cropImageAsk: "画像をクロップしてええか?"
cropYes: "切り抜いたる"
cropNo: "切り抜かへん"
file: "ファイル"
@@ -901,11 +903,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
localOnly: "ローカルのみ"
remoteOnly: "リモートのみ"
failedToUpload: "アップロードに失敗してもうたわ…"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたアップロードできへん。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いアップロードできへん。"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへん。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへん。"
beta: "ベータ"
enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
navbar: "ナビゲーションバー"
shuffle: "シャッフルするで"
@@ -915,10 +917,11 @@ pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知をオンにするで"
unsubscribePushNotification: "プッシュ通知を止めるで"
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す"
caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中やで"
@@ -931,7 +934,7 @@ numberOfLikes: "いいね数"
show: "表示"
neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskey気に入っとっただけましたん"
didYouLikeMisskey: "Misskey気に入ってくれた"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
roles: "ロール"
role: "ロール"
@@ -941,7 +944,7 @@ assign: "アサイン"
unassign: "アサインを解除"
color: "色"
manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作れなさそうや"
youCannotCreateAnymore: "これ以上作れなさそうや"
cannotPerformTemporary: "一時的に利用できへんで"
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
preset: "プリセット"
@@ -960,7 +963,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録するわ"
exploreOtherServers: "他のサーバー見てみる"
letsLookAtTimeline: "タイムライン見てみーや"
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
disableFederationConfirm: "連合なしにしとくか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
disableFederationOk: "連合なしにしとく"
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿"
@@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きする"
noteIdOrUrl: "ートIDかURL"
accountMigration: "アカウントのお引っ越し"
accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
forceShowAds: "常に広告を表示しとく"
_accountMigration:
moveTo: "このアカウントをさらのアカウントに引っ越すで"
moveToLabel: "引っ越し先のアカウント:"
moveAccountDescription: "この操作は戻されへんで。まず引っ越し先のアカウントでこのアカウントへのエイリアスが作れたか確認してきなはれや。エイリアスができてたら、引っ越し先のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃんと確認しーや"
_achievements:
earnedAt: "貰った日ぃ"
_types:
@@ -1043,7 +1061,7 @@ _achievements:
_login7:
title: "ビギナーⅡ"
description: "通算7日ログインした"
flavor: "慣れてきたんちゃう?"
flavor: "慣れてきたんちゃう?"
_login15:
title: "ビギナーⅢ"
description: "通算15日ログインした"
@@ -1147,7 +1165,7 @@ _achievements:
_iLoveMisskey:
title: "Misskey好きやねん"
description: "\"I ❤ #Misskey\"を投稿した"
flavor: "Misskeyを使ってくれてありがとう by 開発チーム"
flavor: "Misskeyを使ってくれておおきに by 開発チーム"
_foundTreasure:
title: "なんでも鑑定団"
description: "隠されたお宝を発見した"
@@ -1173,7 +1191,7 @@ _achievements:
description: "ホームタイムラインの流速が20npmを超す"
_viewInstanceChart:
title: "アナリスト"
description: "インスタンスのチャートを表示した"
description: "サーバーのチャートを表示した"
_outputHelloWorldOnScratchpad:
title: "Hello, world!"
description: "スクラッチパッドで hello worldを出力した"
@@ -1210,7 +1228,7 @@ _achievements:
_loggedInOnNewYearsDay:
title: "あけましておめでとうございます!"
description: "元旦にログインした"
flavor: "今年も弊インスタンスをよろしくお願いします"
flavor: "今年も弊サーバーをよろしゅう頼みます"
_cookieClicked:
title: "クッキー叩くやつ"
description: "クッキー叩いてもうた"
@@ -1225,8 +1243,8 @@ _role:
name: "ロール名"
description: "ロールの説明"
permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はインスタンスの全ての設定を変更できるで。"
assignTarget: "アサインターゲット"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はサーバーの全ての設定を変更できるで。"
assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
manual: "マニュアル"
conditional: "コンディショナル"
@@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canInvite: "インスタンス招待コードの発行"
canInvite: "サーバー招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数"
@@ -1267,7 +1285,7 @@ _role:
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
_condition:
@@ -1320,7 +1338,7 @@ _ad:
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
_gallery:
my: "あんたの投稿"
liked: "いいねした投稿"
@@ -1390,6 +1408,8 @@ _channel:
following: "フォロー中やで"
usersCount: "{n}人が参加中やで"
notesCount: "{n}こ投稿があるで"
nameAndDescription: "名前と説明"
nameOnly: "名前だけ"
_menuDisplay:
sideFull: "横"
sideIcon: "横(アイコン)"
@@ -1405,10 +1425,10 @@ _wordMute:
hard: "ハード"
mutedNotes: "ミュートされたノート"
_instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したインスタンスのノートを隠すで。"
heading: "ミュートするインスタンス"
title: "設定したサーバーのノートを隠すで。"
heading: "ミュートするサーバー"
_theme:
explore: "テーマを探す"
install: "テーマのインストール"
@@ -1630,7 +1650,7 @@ _widgets:
digitalClock: "デジタル時計"
unixClock: "UNIX時計"
federation: "連合"
instanceCloud: "インスタンスクラウド"
instanceCloud: "サーバークラウド"
postForm: "投稿フォーム"
slideshow: "スライドショー"
button: "ボタン"
@@ -1681,7 +1701,7 @@ _visibility:
specified: "ダイレクト"
specifiedDescription: "選んだユーザーのみに公開するで"
disableFederation: "連合なし"
disableFederationDescription: "他インスタンスへは送らんとくわ"
disableFederationDescription: "他サーバーへは送らんとくわ"
_postForm:
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
@@ -1870,6 +1890,7 @@ _deck:
channel: "チャンネル"
mentions: "あんた宛て"
direct: "ダイレクト"
roleTimeline: "ロールタイムライン"
_dialog:
charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}"
charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}"
@@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!"

View File

@@ -1,2 +1 @@
---

View File

@@ -103,4 +103,3 @@ _deck:
_columns:
notifications: "Ilɣuyen"
list: "Tibdarin"

View File

@@ -83,4 +83,3 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ"

View File

@@ -958,7 +958,6 @@ copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기"
@@ -1851,4 +1850,3 @@ _dialog:
_webhookSettings:
name: "이름"
active: "활성화"

View File

@@ -163,11 +163,15 @@ instanceInfo: "ອີນສະແຕນ"
statistics: "ສະຖິຕິ"
clearQueue: "ລ້າງຄິວ"
clearCachedFiles: "ລຶບລ້າງແຄສ"
noUsers: "ບໍ່ພົບຜູ້ໃຊ້"
editProfile: "ແກ້ໄຂໂປຣໄຟລ໌"
done: "ສຳເລັດ"
processing: "ກຳລັງປະມວນຜົນ"
preview: "ສະແດງເປັນຕົວຢ່າງ"
default: "ຄ່າເລີ່ມຕົ້ນ"
defaultValueIs: "ຄ່າເລີ່ມຕົ້ນ: {value}"
noCustomEmojis: "ບໍ່ມີອີໂມຈິ"
noJobs: "ບໍ່ມີຊິ້ນວຽກ"
federating: "ສະຫະພັນ"
blocked: "ບລັອກແລ້ວ "
suspended: "ໂຈະ"
@@ -182,6 +186,9 @@ changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
security: "ຄວາມປອດໄພ"
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
newPassword: "ລະຫັດຜ່ານໃໝ່"
newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
attachFile: "ແນບໄຟລ໌"
more: "ເພີ່ມເຕີມ!"
featured: "ໄຮໄລທ໌"
usernameOrUserId: "ຊື່ຜູ້ໃຊ້ ຫຼື id ຜູ້ໃຊ້"
@@ -196,25 +203,31 @@ saved: "ບັນທຶກແລ້ວ"
messaging: "ແຊ໋ດ"
upload: "ອັບໂຫຼດ"
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
fromDrive: "ຈາກ Drive"
fromUrl: "ຈາກ URL"
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
messageRead: "ອ່ານແລ້ວ"
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
nUsersRead: "ອ່ານໂດຍ {n}"
tos: "ເງື່ອນໄຂການໃຫ້ບໍລິການ"
start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ"
home: "ໜ້າຫຼັກ"
activity: "ກິດຈະກຳ"
images: "ຮູບພາບ"
birthday: "ວັນເກີດ"
yearsOld: "{age} ປີ"
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
location: "ທີ່ຕັ້ງ"
theme: "ແທ໋ມ"
themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
light: "ສະຫວ່າງ"
dark: "ມືດ"
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
darkThemes: "ຮູບແບບສີສັນມືດ"
syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
drive: "ຂັບ"
fileName: "ຊື່ໄຟລ໌"
selectFile: "ເລືອກໄຟລ໌"
@@ -265,6 +278,9 @@ invite: "ເຊີນ"
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
turnstileSiteKey: "ກະແຈໄຊທ໌"
turnstileSecretKey: "ກະແຈລັບ"
name: "ຊື່"
userList: "ລາຍການ"
about: "ກ່ຽວກັບ"
aboutMisskey: "ກ່ຽວກັບ Misskey"
@@ -326,6 +342,7 @@ _widgets:
instanceInfo: "ອີນສະແຕນ"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
activity: "ກິດຈະກຳ"
federation: "ສະຫະພັນ"
_userList:
chooseList: "ເລືອກບັນຊີລາຍການ"
@@ -335,6 +352,7 @@ _visibility:
home: "ໜ້າຫຼັກ"
followers: "ຜູ້ຕິດຕາມ"
_profile:
name: "ຊື່"
username: "ຊື່ຜູ້ໃຊ້"
_exportOrImport:
followingList: "ກຳລັງຕິດຕາມ"
@@ -368,4 +386,5 @@ _deck:
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"
_webhookSettings:
name: "ຊື່"

View File

@@ -485,4 +485,3 @@ _deck:
mentions: "Vermeldingen"
_webhookSettings:
name: "Naam"

View File

@@ -1,3 +1,2 @@
---
_lang_: "Norsk Bokmål"

View File

@@ -1379,4 +1379,3 @@ _deck:
_webhookSettings:
name: "Nazwa"
active: "Właczono"

View File

@@ -557,4 +557,3 @@ _deck:
direct: "Notas diretas"
_webhookSettings:
name: "Nome"

View File

@@ -703,4 +703,3 @@ _deck:
mentions: "Mențiuni"
_webhookSettings:
name: "Nume"

View File

@@ -950,7 +950,6 @@ copyErrorInfo: "Скопировать код ошибки"
joinThisServer: "Присоединяйтесь к этому серверу"
exploreOtherServers: "Искать другие сервера"
letsLookAtTimeline: "Давайте посмотрим на ленту"
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
_achievements:
earnedAt: "Разблокировано в"
_types:
@@ -1839,4 +1838,3 @@ _dialog:
_webhookSettings:
name: "Название"
active: "Вкл."

View File

@@ -1,2 +1 @@
---

View File

@@ -1477,4 +1477,3 @@ _deck:
_webhookSettings:
name: "Názov"
active: "Zapnuté"

View File

@@ -343,9 +343,16 @@ recentlyRegisteredUsers: "Nyligen registrerade användare"
userList: "Listor"
aboutMisskey: "Om Misskey"
administrator: "Administratör"
passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey."
resetPassword: "Återställ Lösenord"
newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela"
enable: "Aktivera"
weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord"
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare"
@@ -354,11 +361,14 @@ enableEmail: "Aktivera epost-utskick"
smtpHost: "Värd"
smtpUser: "Användarnamn"
smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
clearCache: "Rensa cache"
onlineUsersCount: "{n} användare är online"
enabled: "Aktiverad"
user: "Användare"
global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder"
incorrectPassword: "Fel lösenord."
searchByGoogle: "Sök"
file: "Filer"
enableAutoSensitive: "Automatisk NSFW markering"
@@ -368,6 +378,15 @@ subscribePushNotification: "Aktivera pushnotiser"
unsubscribePushNotification: "Avaktivera pushnotiser"
pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade"
pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser"
windowMaximize: "Maximera"
windowMinimize: "Minimera"
windowRestore: "Återställ"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
_achievements:
_types:
_open3windows:
title: "Flera Fönster"
description: "Ha minst 3 fönster öppna samtidigt"
_email:
_follow:
title: "följde dig"
@@ -384,6 +403,7 @@ _sfx:
chat: "Chatt"
antenna: "Antenner"
_2fa:
passwordToTOTP: "Skriv in ditt lösenord"
renewTOTPCancel: "Nej tack"
_antennaSources:
all: "Alla noter"
@@ -444,4 +464,3 @@ _deck:
mentions: "Omnämningar"
_webhookSettings:
active: "Aktiverad"

View File

@@ -122,6 +122,8 @@ unmarkAsSensitive: "ยกเลิกทำเครื่องหมายเ
enterFileName: "พิมพ์ชื่อไฟล์"
mute: "ปิดเสียง"
unmute: "ยกเลิกการปิดเสียง"
renoteMute: "ปิดเสียงรีโน้ต"
renoteUnmute: "เปิดเสียง รีโน้ต"
block: "บล็อค"
unblock: "เลิกปิดกั้น"
suspend: "ถูกระงับ"
@@ -153,6 +155,7 @@ flagShowTimelineReplies: "แสดงตอบกลับ ในไทม์
flagShowTimelineRepliesDescription: "แสดงการตอบกลับของผู้ใช้งานไปยังโน้ตของผู้ใช้งานรายอื่นๆในไทม์ไลน์หากได้เปิดเอาไว้"
autoAcceptFollowed: "อนุมัติคำขอติดตามโดยอัตโนมัติทันที จากผู้ใช้งานที่คุณกำลังติดตาม"
addAccount: "เพิ่มบัญชี"
reloadAccountsList: "รีโหลดรายการบัญชีใหม่"
loginFailed: "การเข้าสู่ระบบไม่สำเร็จ"
showOnRemote: "ดูบนอินสแตนซ์ระยะไกล"
general: "ทั่วไป"
@@ -503,6 +506,7 @@ objectStorageUseSSLDesc: "ปิดการทำงานนี้ไว้
objectStorageUseProxy: "เชื่อมต่อผ่านพร็อกซี"
objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหากคุณจะไม่ใช้ Proxy สำหรับการเชื่อมต่อ API"
objectStorageSetPublicRead: "ตั้งค่า \"public-read\" ในการอัปโหลด"
s3ForcePathStyleDesc: "ถ้าหากเปิดใช้งาน s3ForcePathStyle ชื่อบัคเก็ตนั้นอาจจะต้องรวมอยู่ในเส้นทางของ URL ซึ่งตรงข้ามกับชื่อโฮสต์ของ URL คุณอาจจะต้องเปิดใช้งานการตั้งค่านี้เมื่อใช้บริการต่างๆ เช่น อินสแตนซ์ Minio ที่โฮสต์เองนะ"
serverLogs: "บันทึกของเซิร์ฟเวอร์"
deleteAll: "ลบทั้งหมด"
showFixedPostForm: "แสดงแบบฟอร์มการโพสต์ที่ด้านบนสุดของไทม์ไลน์"
@@ -545,7 +549,9 @@ userSilenced: "ผู้ใช้รายนี้กำลังถูกป
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
tokenRevoked: "โทเค็นไม่ถูกต้อง"
tokenRevokedDescription: "โทเค็นนี้หมดอายุแล้วนะค่ะกรุณาเข้าสู่ระบบอีกครั้งนะ"
accountDeleted: "ลบบัญชีแล้ว"
accountDeletedDescription: "บัญชีนี้ถูกลบไปแล้วนะ"
menu: "เมนู"
divider: "ตัวแบ่ง"
addItem: "เพิ่มรายการ"
@@ -914,6 +920,7 @@ pushNotificationNotSupported: "เบราว์เซอร์หรืออ
sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
sendPushNotificationReadMessageCaption: "การแจ้งเตือนที่มีข้อความ \"{emptyPushNotificationMessage}\" จะแสดงขึ้นมาในช่วงระยะเวลาสั้นๆ การดำเนินการนี้อาจทำให้เพิ่มการใช้งานแบตเตอรี่ของอุปกรณ์ถ้าหากมีนะ"
windowMaximize: "ขยายใหญ่สุดแล้ว"
windowMinimize: "ย่อเล็กที่สุด"
windowRestore: "เลิกทำ"
caption: "รายละเอียด"
loggedInAsBot: "ล็อกอินเป็นบอตอยู่ในขณะนี้"
@@ -955,12 +962,17 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ"
disableFederationOk: "ปิดการใช้งาน"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
reactionAcceptance: "การยอมรับรีแอคชั่น"
likeOnly: "ที่ชอบเท่านั้น"
likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น"
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
sensitiveWords: "คำที่ละเอียดอ่อน"
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
@@ -972,6 +984,22 @@ drivecleaner: "ทำความสะอาดไดรฟ์"
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล"
noteIdOrUrl: "โน้ต ID หรือ URL"
accountMigration: "การโยกย้ายบัญชี"
accountMoved: "ผู้ใช้รายนี้ได้ย้ายไปยังบัญชีใหม่แล้ว:"
forceShowAds: "แสดงโฆษณาเสมอ"
_accountMigration:
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
moveToLabel: "บัญชีที่จะย้ายไปที่:"
moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com"
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
moveFromLabel: "บัญชีที่จะย้ายจาก:"
moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ"
_achievements:
earnedAt: "ได้รับเมื่อ"
_types:
@@ -1268,6 +1296,8 @@ _role:
followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n"
followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ"
followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ"
notesLessThanOrEq: "จำนวนโพสต์น้อยกว่าเท่ากับ"
notesMoreThanOrEq: "จำนวนโพสต์มากกว่าเท่ากับ"
and: "และ"
or: "หรือ"
not: "ไม่"
@@ -1867,6 +1897,16 @@ _drivecleaner:
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
_webhookSettings:
createWebhook: "สร้าง Webhook"
name: "ชื่อ"
secret: "ความลับ"
events: "อีเว้นท์ Webhook"
active: "เปิดใช้งาน"
_events:
follow: "เมื่อกำลังติดตามผู้ใช้"
followed: "เมื่อกำลังติดตามแล้ว"
note: "เมื่อกำลังโพสต์โน้ต"
reply: "เมื่อได้รับการตอบกลับ"
renote: "รีโน้ตแล้วเมื่อ"
reaction: "เมื่อได้รับรีแอคชั่น"
mention: "เมื่อกำลังถูกกล่าวถึง"

View File

@@ -60,4 +60,3 @@ _deck:
_columns:
notifications: "Bildirim"
tl: "Zaman çizelgesi"

View File

@@ -2,4 +2,3 @@
_lang_: "ياپونچە"
search: "ئىزدەش"
searchByGoogle: "ئىزدەش"

View File

@@ -1641,4 +1641,3 @@ _deck:
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"

View File

@@ -1707,4 +1707,3 @@ _dialog:
_webhookSettings:
name: "Tên"
active: "Đã bật"

View File

@@ -16,16 +16,17 @@ cancel: "取消"
noThankYou: "不用,谢谢"
enterUsername: "输入用户名"
renotedBy: "由 {user} 转贴"
noNotes: "没有帖"
noNotes: "没有帖"
noNotifications: "无通知"
instance: "服务器"
settings: "设置"
notificationSettings: "通知设置"
basicSettings: "基本设置"
otherSettings: "其他设置"
openInWindow: "在新窗口中打开"
profile: "个人资料"
timeline: "时间线"
noAccountDescription: "这个人很懒,没有写自我介绍"
noAccountDescription: "此用户尚无自我介绍"
login: "登录"
loggingIn: "正在登录..."
logout: "登出"
@@ -60,7 +61,7 @@ youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "您收到了关注请求"
followRequestAccepted: "您的关注请求被通过了"
mention: "提及"
mentions: "提"
mentions: "提到我的"
directNotes: "私信"
importAndExport: "导入和导出"
import: "导入"
@@ -85,7 +86,7 @@ somethingHappened: "出现了一些问题!"
retry: "重试"
pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
serverIsDead: "没有服务器响应。 请稍后再试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称"
privacy: "隐私"
@@ -95,7 +96,7 @@ follow: "关注"
followRequest: "关注申请"
followRequests: "关注申请"
unfollow: "取消关注"
followRequestPending: "发送关注请求"
followRequestPending: "关注请求批准中"
enterEmoji: "输入表情符号"
renote: "转发"
unrenote: "取消转发"
@@ -119,7 +120,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "删除附件"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名"
enterFileName: "输入文件名"
mute: "屏蔽"
unmute: "解除屏蔽"
renoteMute: "屏蔽转帖"
@@ -142,18 +143,18 @@ customEmojis: "自定义表情符号"
emoji: "表情符号"
emojis: "表情符号"
emojiName: "表情符号名称"
emojiUrl: "表情符号地址"
emojiUrl: "emoji 地址"
addEmoji: "添加表情符号"
settingGuide: "推荐配置"
cacheRemoteFiles: "远程文件缓存"
cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。"
flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。"
flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注者的关注"
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败"
@@ -203,7 +204,7 @@ blockedInstances: "被阻拦的服务器"
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户"
blockedUsers: "拉黑的用户"
blockedUsers: "拉黑的用户"
noUsers: "无用户"
editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?"
@@ -336,7 +337,7 @@ enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
registration: "注册"
enableRegistration: "允许新用户注册"
enableRegistration: "允许任何人注册"
invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘空间"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
@@ -354,7 +355,7 @@ pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥"
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
hcaptchaSecretKey: "密钥"
recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥"
@@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接请关闭。"
objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。"
objectStorageSetPublicRead: "上传时设置为public-read"
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
serverLogs: "服务器日志"
deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框"
@@ -919,6 +921,7 @@ pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息"
sendPushNotificationReadMessage: "删除已读推送通知消息"
sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "还原"
caption: "标题"
loggedInAsBot: "以Bot账户登录"
@@ -960,7 +963,9 @@ copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线"
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
disableFederationConfirm: "确定要禁用联合?"
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
disableFederationOk: "联合禁用"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道"
@@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
largeNoteReactions: "使用大图标来显示回应"
noteIdOrUrl: "帖子ID或URL"
accountMigration: "账户迁移"
accountMoved: "此用户已迁移账户"
forceShowAds: "总是显示广告"
_accountMigration:
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
moveFrom: "从别的账号迁移到此账户"
moveFromLabel: "迁移前的账户"
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
_achievements:
earnedAt: "达成时间"
_types:
@@ -1255,7 +1273,7 @@ _role:
gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖"
canInvite: "发放实例邀请码"
canInvite: "发放服务器邀请码"
canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量"
pinMax: "帖子置顶数量限制"
@@ -1390,6 +1408,8 @@ _channel:
following: "正在关注"
usersCount: "有{n}人参与"
notesCount: "有{n}个帖子"
nameAndDescription: "名称与描述"
nameOnly: "仅名称"
_menuDisplay:
sideFull: "横向"
sideIcon: "横向(图标)"
@@ -1629,7 +1649,7 @@ _widgets:
photos: "照片"
digitalClock: "数字时钟"
unixClock: "UNIX时钟"
federation: "联邦宇宙"
federation: "联"
instanceCloud: "服务器云"
postForm: "投稿窗口"
slideshow: "幻灯片展示"
@@ -1870,6 +1890,7 @@ _deck:
channel: "频道"
mentions: "提及"
direct: "指定用户"
roleTimeline: "角色时间线"
_dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
@@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@@ -20,6 +20,7 @@ noNotes: "無貼文。"
noNotifications: "沒有通知"
instance: "實例"
settings: "設定"
notificationSettings: "通知選項"
basicSettings: "基本設定"
otherSettings: "其他設定"
openInWindow: "在新視窗開啟"
@@ -506,6 +507,7 @@ objectStorageUseSSLDesc: "如果不使用https進行API連接請關閉"
objectStorageUseProxy: "使用網路代理"
objectStorageUseProxyDesc: "如果不使用代理進行API連接請關閉"
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 會強制將儲存槽名稱指定為 URL 中路徑的一部分,而不是主機名。 使用自託管 Minio 之類的可能需要啟用。"
serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
@@ -560,7 +562,7 @@ inboxUrl: "收件夾URL"
addedRelays: "已加入的中繼"
serviceworkerInfo: "您需要啟用推送通知"
deletedNote: "已删除的貼文"
invisibleNote: "隱藏的貼文"
invisibleNote: "私密的貼文"
enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性"
poll: "投票"
@@ -919,6 +921,7 @@ pushNotificationNotSupported: "瀏覽器或實例不支援推播通知"
sendPushNotificationReadMessage: "通知與訊息如果已讀的話,就將推播通知刪除"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會增加設備的電池消耗。"
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "復原"
caption: "標題"
loggedInAsBot: "以機器人帳戶登入中"
@@ -960,7 +963,9 @@ copyErrorInfo: "複製錯誤資訊"
joinThisServer: "在此伺服器上註冊"
exploreOtherServers: "探索其他伺服器"
letsLookAtTimeline: "看看時間軸"
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
disableFederationConfirm: "要停止聯邦功能嗎?"
disableFederationConfirmWarn: "即使停止了聯邦功能,貼文也不會變成私密的。在大部分的情況下,沒有必要停止聯邦功能。"
disableFederationOk: "停止聯邦功能"
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道"
@@ -983,6 +988,19 @@ retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
largeNoteReactions: "將貼文的反應放大顯示"
noteIdOrUrl: "貼文ID或URL"
accountMigration: "遷移帳戶"
accountMoved: "這個使用者已遷移至新的帳戶:"
forceShowAds: "總是顯示廣告"
_accountMigration:
moveTo: "將這個帳戶遷移至新的帳戶"
moveToLabel: "要遷移到的帳戶:"
moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com"
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromLabel: "要遷移過來的帳戶:"
moveFromDescription: "如果你想把跟隨者從別的帳戶遷移過來,必須先在這裡建立別名。請務必在執行遷移之前建立別名!請像這樣輸入要遷移的帳戶:@person@instance.com"
migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外請確認在要遷移到的帳戶已經建立了一個別名。"
_achievements:
earnedAt: "獲得日期"
_types:
@@ -1390,6 +1408,8 @@ _channel:
following: "關注中"
usersCount: "有{n}人參與"
notesCount: "有{n}個貼文"
nameAndDescription: "名稱與說明"
nameOnly: "僅名稱"
_menuDisplay:
sideFull: "側向"
sideIcon: "側向(圖示)"
@@ -1870,6 +1890,7 @@ _deck:
channel: "頻道"
mentions: "提及"
direct: "指定使用者"
roleTimeline: "角色時間軸"
_dialog:
charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}"
charactersBelow: "低於最少字數!現在 {current} / 限制 {max}"
@@ -1893,4 +1914,3 @@ _webhookSettings:
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.11.0.beta-1",
"version": "13.11.3",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@8.1.0",
"packageManager": "pnpm@8.1.1",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -16,6 +16,7 @@
"scripts": {
"build-pre": "node ./scripts/build-pre.js",
"build": "pnpm build-pre && pnpm -r build && pnpm gulp",
"build-storybook": "pnpm --filter frontend build-storybook",
"start": "pnpm check:connect && 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",
@@ -50,13 +51,13 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "5.0.2"
"typescript": "5.0.3"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"cross-env": "7.0.3",
"cypress": "12.9.0",
"eslint": "8.37.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,8 +1,15 @@
import Redis from 'ioredis';
import { loadConfig } from './built/config.js';
import { createRedisConnection } from './built/redis.js';
const config = loadConfig();
const redis = createRedisConnection(config);
const redis = new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => {

View File

@@ -0,0 +1,17 @@
export class AvatarUrlAndBannerUrl1680775031481 {
name = 'AvatarUrlAndBannerUrl1680775031481'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`);
}
}

View File

@@ -0,0 +1,17 @@
export class AccountMove1680931179228 {
name = 'AccountMove1680931179228'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "movedToUri" character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" ADD "alsoKnownAs" text`);
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAs"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToUri"`);
}
}

View File

@@ -22,24 +22,24 @@
"test-and-coverage": "pnpm jest-and-coverage"
},
"optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.42",
"@swc/core-darwin-x64": "^1.3.42",
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
"@swc/core-linux-arm64-gnu": "^1.3.42",
"@swc/core-linux-arm64-musl": "^1.3.42",
"@swc/core-linux-x64-gnu": "^1.3.42",
"@swc/core-linux-x64-musl": "^1.3.42",
"@swc/core-win32-arm64-msvc": "^1.3.42",
"@swc/core-win32-ia32-msvc": "^1.3.42",
"@swc/core-win32-x64-msvc": "^1.3.42",
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.3.46",
"@swc/core-darwin-x64": "1.3.46",
"@swc/core-linux-arm-gnueabihf": "1.3.46",
"@swc/core-linux-arm64-gnu": "1.3.46",
"@swc/core-linux-arm64-musl": "1.3.46",
"@swc/core-linux-x64-gnu": "1.3.46",
"@swc/core-linux-x64-musl": "1.3.46",
"@swc/core-win32-arm64-msvc": "1.3.46",
"@swc/core-win32-ia32-msvc": "1.3.46",
"@swc/core-win32-x64-msvc": "1.3.46",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@aws-sdk/client-s3": "3.301.0",
"@aws-sdk/lib-storage": "3.301.0",
"@aws-sdk/node-http-handler": "3.296.0",
"@aws-sdk/client-s3": "3.306.0",
"@aws-sdk/lib-storage": "3.306.0",
"@aws-sdk/node-http-handler": "3.306.0",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
@@ -49,15 +49,15 @@
"@fastify/cors": "8.2.1",
"@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0",
"@fastify/static": "6.10.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.12",
"@nestjs/core": "9.3.12",
"@nestjs/testing": "9.3.12",
"@nestjs/common": "9.4.0",
"@nestjs/core": "9.4.0",
"@nestjs/testing": "9.4.0",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.42",
"@swc/core": "1.3.46",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@@ -136,8 +136,8 @@
"tsc-alias": "1.8.5",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typescript": "5.0.2",
"typeorm": "0.3.13",
"typescript": "5.0.3",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
@@ -190,8 +190,8 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3",
"eslint": "8.37.0",

View File

@@ -2,18 +2,15 @@ import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
import Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const config = loadConfig();
const $config: Provider = {
provide: DI.config,
useValue: config,
useValue: loadConfig(),
};
const $db: Provider = {
@@ -28,18 +25,47 @@ const $db: Provider = {
const $redis: Provider = {
provide: DI.redis,
useFactory: (config) => {
const redisClient = createRedisConnection(config);
return redisClient;
return new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
},
inject: [DI.config],
};
const $redisSubscriber: Provider = {
provide: DI.redisSubscriber,
const $redisForPub: Provider = {
provide: DI.redisForPub,
useFactory: (config) => {
const redisSubscriber = createRedisConnection(config);
redisSubscriber.subscribe(config.host);
return redisSubscriber;
const redis = new Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
return redis;
},
inject: [DI.config],
};
const $redisForSub: Provider = {
provide: DI.redisForSub,
useFactory: (config) => {
const redis = new Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
redis.subscribe(config.host);
return redis;
},
inject: [DI.config],
};
@@ -47,14 +73,15 @@ const $redisSubscriber: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisSubscriber],
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
providers: [$config, $db, $redis, $redisForPub, $redisForSub],
exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@Inject(DI.db) private db: DataSource,
@Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis,
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
) {}
async onApplicationShutdown(signal: string): Promise<void> {
@@ -69,7 +96,8 @@ export class GlobalModule implements OnApplicationShutdown {
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),
this.redisSubscriber.disconnect(),
this.redisForPub.disconnect(),
this.redisForSub.disconnect(),
]);
}
}

View File

@@ -25,6 +25,14 @@ export type Source = {
disableCache?: boolean;
extra?: { [x: string]: string };
};
dbReplications?: boolean;
dbSlaves?: {
host: string;
port: number;
db: string;
user: string;
pass: string;
}[];
redis: {
host: string;
port: number;
@@ -33,6 +41,22 @@ export type Source = {
db?: number;
prefix?: string;
};
redisForPubsub?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForJobQueue?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
elasticsearch: {
host: string;
port: number;
@@ -91,6 +115,8 @@ export type Mixin = {
mediaProxy: string;
externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
};
export type Config = Source & Mixin;
@@ -151,6 +177,8 @@ export function loadConfig() {
: null;
if (!config.redis.prefix) config.redis.prefix = mixin.host;
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
return Object.assign(config, mixin);
}

View File

@@ -0,0 +1,114 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { LocalUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { RelayService } from '@/core/RelayService.js';
@Injectable()
export class AccountMoveService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
private accountUpdateService: AccountUpdateService,
private relayService: RelayService,
) {
}
/**
* Move a local account to a remote account.
*
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
*/
@bindThis
public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
// Make sure that the destination is a remote account.
if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
if (!dst.uri) throw new Error('destination uri is empty');
// add movedToUri to indicate that the user has moved
const update = {} as Partial<User>;
update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
update.movedToUri = dst.uri;
await this.usersRepository.update(src.id, update);
const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
await this.apDeliverManagerService.deliverToFollowers(src, updateAct);
this.relayService.deliverToRelays(src, updateAct);
// Deliver Move activity to the followers of the old account
const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst));
await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// follow the new account and unfollow the old one
const followings = await this.followingsRepository.find({
relations: {
follower: true,
},
where: {
followeeId: src.id,
followerHost: IsNull(), // follower is local
},
});
for (const following of followings) {
if (!following.follower) continue;
try {
await this.userFollowingService.follow(following.follower, dst);
await this.userFollowingService.unfollow(following.follower, src);
} catch {
/* empty */
}
}
return iObj;
}
/**
* Create an alias of an old remote account.
*
* The user's new profile will be published to the followers.
*/
@bindThis
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
await this.usersRepository.update(me.id, updates);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
detail: true,
includeSecrets: true,
});
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
if (me.isLocked === false) {
await this.userFollowingService.acceptAllFollowRequests(me);
}
this.accountUpdateService.publishToFollowers(me.id);
return iObj;
}
}

View File

@@ -29,7 +29,7 @@ export class AccountUpdateService {
public async publishToFollowers(userId: User['id']) {
const user = await this.usersRepository.findOneBy({ id: userId });
if (user == null) throw new Error('user not found');
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));

View File

@@ -27,8 +27,8 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
this.antennasFetched = false;
this.antennas = [];
this.redisSubscriber.on('message', this.onRedisMessage);
this.redisForSub.on('message', this.onRedisMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onRedisMessage);
this.redisForSub.off('message', this.onRedisMessage);
}
@bindThis
@@ -91,14 +91,24 @@ export class AntennaService implements OnApplicationShutdown {
}
@bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
this.redisClient.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise<void> {
const antennas = await this.getAntennas();
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
const redisPipeline = this.redisClient.pipeline();
for (const antenna of matchedAntennas) {
redisPipeline.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
'*',
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
}
redisPipeline.exec();
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View File

@@ -27,8 +27,8 @@ export class CacheService implements OnApplicationShutdown {
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -116,7 +116,7 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@@ -167,6 +167,6 @@ export class CacheService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
import { AntennaService } from './AntennaService.js';
@@ -119,6 +120,7 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
@@ -242,6 +244,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
],
providers: [
LoggerService,
AccountMoveService,
AccountUpdateService,
AiService,
AntennaService,
@@ -359,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
$AntennaService,
@@ -477,6 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [
QueueModule,
LoggerService,
AccountMoveService,
AccountUpdateService,
AiService,
AntennaService,
@@ -593,6 +598,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
$AiService,
$AntennaService,

View File

@@ -1,24 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import type { EmojisRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
@Injectable()
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config)
private config: Config,
@@ -32,9 +37,22 @@ export class CustomEmojiService {
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) {
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
fromRedisConverter: (value) => {
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
return new Map(JSON.parse(value).map((x: Serialized<Emoji>) => [x.name, {
...x,
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
}]));
},
});
}
@bindThis
@@ -60,7 +78,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
await this.db.queryResultCache?.remove(['meta_emojis']);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
@@ -70,6 +88,146 @@ export class CustomEmojiService {
return emoji;
}
@bindThis
public async update(id: Emoji['id'], data: {
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
}): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
});
this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
}
@bindThis
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(aliases))],
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
aliases: aliases,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
category: category,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async delete(id: Emoji['id']) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
}
@bindThis
public async deleteBulk(ids: Emoji['id'][]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
}
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
@@ -84,7 +242,7 @@ export class CustomEmojiService {
}
@bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
@@ -116,16 +274,7 @@ export class CustomEmojiService {
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
const url = isLocal
? emojiUrl
: this.config.proxyRemoteFiles
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
: emojiUrl;
return url;
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
}
/**
@@ -143,30 +292,6 @@ export class CustomEmojiService {
return res;
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/**
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
*/

View File

@@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -9,23 +10,41 @@ import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService {
private cache: MemoryKVCache<Instance>;
public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private utilityService: UtilityService,
private idService: IdService,
) {
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => {
const parsed = JSON.parse(value);
if (parsed == null) return null;
return {
...parsed,
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
};
},
});
}
@bindThis
public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host);
const cached = this.cache.get(host);
const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host });
@@ -37,10 +56,10 @@ export class FederatedInstanceService {
firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.cache.set(host, i);
this.federatedInstanceCache.set(host, i);
return i;
} else {
this.cache.set(host, index);
this.federatedInstanceCache.set(host, index);
return index;
}
}
@@ -49,10 +68,10 @@ export class FederatedInstanceService {
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
host = this.utilityService.toPuny(host);
const cached = this.cache.get(host);
const cached = await this.federatedInstanceCache.get(host);
if (cached == null) return;
this.cache.set(host, {
this.federatedInstanceCache.set(host, {
...cached,
...data,
});

View File

@@ -14,11 +14,13 @@ import type {
MainStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
RoleTimelineStreamTypes,
} from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { Role } from '@/models';
@Injectable()
export class GlobalEventService {
@@ -26,8 +28,8 @@ export class GlobalEventService {
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForPub)
private redisForPub: Redis.Redis,
) {
}
@@ -37,7 +39,7 @@ export class GlobalEventService {
{ type: type, body: null } :
{ type: type, body: value };
this.redisClient.publish(this.config.host, JSON.stringify({
this.redisForPub.publish(this.config.host, JSON.stringify({
channel: channel,
message: message,
}));
@@ -81,6 +83,11 @@ export class GlobalEventService {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishNotesStream(note: Packed<'Note'>): void {
this.publish('notesStream', null, note);

View File

@@ -3,10 +3,11 @@ import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js';
import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js';
@Injectable()
export class IdService {
@@ -37,11 +38,10 @@ export class IdService {
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
case 'objectid':
case 'meid': return parseMeid(id);
case 'meidg': return parseMeidg(id);
case 'ulid': return parseUlid(id);
default: throw new Error('unrecognized id generation method');
}
}

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: MemoryCache<LocalUser>;
private cache: MemorySingleCache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@@ -19,7 +19,7 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new MemoryCache<LocalUser>(Infinity);
this.cache = new MemorySingleCache<LocalUser>(Infinity);
}
@bindThis

View File

@@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
}, 1000 * 60 * 5);
}
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
clearInterval(this.intervalId);
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -329,7 +329,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'*',
'note', note.id);
}
@@ -493,14 +493,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
});
// Antenna
for (const antenna of (await this.antennaService.getAntennas())) {
this.antennaService.checkHitAntenna(antenna, note, user).then(hit => {
if (hit) {
this.antennaService.addNoteToAntenna(antenna, note, user);
}
});
}
this.antennaService.addNoteToAntennas(note, user);
if (data.reply) {
this.saveReply(data.reply, note);
@@ -554,6 +547,8 @@ export class NoteCreateService implements OnApplicationShutdown {
this.globalEventService.publishNotesStream(noteObj);
this.roleService.addNoteToRoleTimeline(noteObj);
this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {

View File

@@ -66,6 +66,7 @@ export class NotificationService implements OnApplicationShutdown {
@bindThis
private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
}
@bindThis
@@ -99,7 +100,7 @@ export class NotificationService implements OnApplicationShutdown {
const redisIdPromise = this.redisClient.xadd(
`notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300',
`${this.idService.parse(notification.id).date.getTime()}-*`,
'*',
'data', JSON.stringify(notification));
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});

View File

@@ -1,12 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import type { SwSubscriptionsRepository } from '@/models/index.js';
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { RedisKVCache } from '@/misc/cache.js';
// Defined also packages/sw/types.ts#L13
type PushNotificationsTypes = {
@@ -15,6 +17,7 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
'readAllNotifications': undefined;
};
// Reduce length because push message servers have character limits
@@ -40,15 +43,27 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
@Injectable()
export class PushNotificationService {
private subscriptionsCache: RedisKVCache<SwSubscription[]>;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
private metaService: MetaService,
) {
this.subscriptionsCache = new RedisKVCache<SwSubscription[]>(this.redisClient, 'userSwSubscriptions', {
lifetime: 1000 * 60 * 60 * 1, // 1h
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
}
@bindThis
@@ -62,12 +77,13 @@ export class PushNotificationService {
meta.swPublicKey,
meta.swPrivateKey);
// Fetch
const subscriptions = await this.swSubscriptionsRepository.findBy({
userId: userId,
});
const subscriptions = await this.subscriptionsCache.fetch(userId);
for (const subscription of subscriptions) {
if ([
'readAllNotifications',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {

View File

@@ -3,18 +3,18 @@ import Bull from 'bull';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from '../queue/types.js';
import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js';
function q<T>(config: Config, name: string, limitPerSec = -1) {
return new Bull<T>(name, {
redis: {
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
db: config.redis.db ?? 0,
port: config.redisForJobQueue.port,
host: config.redisForJobQueue.host,
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
password: config.redisForJobQueue.pass,
db: config.redisForJobQueue.db ?? 0,
},
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
limiter: limitPerSec > 0 ? {
max: limitPerSec,
duration: 1000,
@@ -41,7 +41,8 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue<DbJobData>;
export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
@@ -75,6 +76,12 @@ const $db: Provider = {
inject: [DI.config],
};
const $relationship: Provider = {
provide: 'queue:relationship',
useFactory: (config: Config) => q(config, 'relationship'),
inject: [DI.config],
};
const $objectStorage: Provider = {
provide: 'queue:objectStorage',
useFactory: (config: Config) => q(config, 'objectStorage'),
@@ -96,6 +103,7 @@ const $webhookDeliver: Provider = {
$deliver,
$inbox,
$db,
$relationship,
$objectStorage,
$webhookDeliver,
],
@@ -105,6 +113,7 @@ const $webhookDeliver: Provider = {
$deliver,
$inbox,
$db,
$relationship,
$objectStorage,
$webhookDeliver,
],

View File

@@ -6,9 +6,10 @@ import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { ThinUser } from '../queue/types.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
import Bull from 'bull';
@Injectable()
export class QueueService {
@@ -21,6 +22,7 @@ export class QueueService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {}
@@ -56,7 +58,7 @@ export class QueueService {
activity: activity,
signature,
};
return this.inboxQueue.add(data, {
attempts: this.config.inboxJobMaxAttempts ?? 8,
timeout: 5 * 60 * 1000, // 5min
@@ -71,7 +73,7 @@ export class QueueService {
@bindThis
public createDeleteDriveFilesJob(user: ThinUser) {
return this.dbQueue.add('deleteDriveFiles', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -81,7 +83,7 @@ export class QueueService {
@bindThis
public createExportCustomEmojisJob(user: ThinUser) {
return this.dbQueue.add('exportCustomEmojis', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -91,7 +93,7 @@ export class QueueService {
@bindThis
public createExportNotesJob(user: ThinUser) {
return this.dbQueue.add('exportNotes', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -101,7 +103,7 @@ export class QueueService {
@bindThis
public createExportFavoritesJob(user: ThinUser) {
return this.dbQueue.add('exportFavorites', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -111,7 +113,7 @@ export class QueueService {
@bindThis
public createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) {
return this.dbQueue.add('exportFollowing', {
user: user,
user: { id: user.id },
excludeMuting,
excludeInactive,
}, {
@@ -123,7 +125,7 @@ export class QueueService {
@bindThis
public createExportMuteJob(user: ThinUser) {
return this.dbQueue.add('exportMuting', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -133,7 +135,7 @@ export class QueueService {
@bindThis
public createExportBlockingJob(user: ThinUser) {
return this.dbQueue.add('exportBlocking', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -143,7 +145,7 @@ export class QueueService {
@bindThis
public createExportUserListsJob(user: ThinUser) {
return this.dbQueue.add('exportUserLists', {
user: user,
user: { id: user.id },
}, {
removeOnComplete: true,
removeOnFail: true,
@@ -153,7 +155,7 @@ export class QueueService {
@bindThis
public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importFollowing', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@@ -161,10 +163,16 @@ export class QueueService {
});
}
@bindThis
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
return this.dbQueue.addBulk(jobs);
}
@bindThis
public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importMuting', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@@ -175,7 +183,7 @@ export class QueueService {
@bindThis
public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importBlocking', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@@ -183,10 +191,32 @@ export class QueueService {
});
}
@bindThis
public createImportBlockingToDbJob(user: ThinUser, targets: string[]) {
const jobs = targets.map(rel => this.generateToDbJobData('importBlockingToDb', { user, target: rel }));
return this.dbQueue.addBulk(jobs);
}
@bindThis
private generateToDbJobData<T extends 'importFollowingToDb' | 'importBlockingToDb', D extends DbJobData<T>>(name: T, data: D): {
name: string,
data: D,
opts: Bull.JobOptions,
} {
return {
name,
data,
opts: {
removeOnComplete: true,
removeOnFail: true,
},
};
}
@bindThis
public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importUserLists', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@@ -197,7 +227,7 @@ export class QueueService {
@bindThis
public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importCustomEmojis', {
user: user,
user: { id: user.id },
fileId: fileId,
}, {
removeOnComplete: true,
@@ -208,7 +238,7 @@ export class QueueService {
@bindThis
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
return this.dbQueue.add('deleteAccount', {
user: user,
user: { id: user.id },
soft: opts.soft,
}, {
removeOnComplete: true,
@@ -216,6 +246,51 @@ export class QueueService {
});
}
@bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
public createUnblockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
const jobs = blockings.map(rel => this.generateRelationshipJobData('unblock', rel));
return this.relationshipQueue.addBulk(jobs);
}
@bindThis
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData): {
name: string,
data: RelationshipJobData,
opts: Bull.JobOptions,
} {
return {
name,
data: {
from: { id: data.from.id },
to: { id: data.to.id },
silent: data.silent,
requestId: data.requestId,
},
opts: {
removeOnComplete: true,
removeOnFail: true,
},
};
}
@bindThis
public createDeleteObjectStorageFileJob(key: string) {
return this.objectStorageQueue.add('deleteFile', {
@@ -246,7 +321,7 @@ export class QueueService {
createdAt: Date.now(),
eventId: uuid(),
};
return this.webhookDeliverQueue.add(data, {
attempts: 4,
timeout: 1 * 60 * 1000, // 1min
@@ -264,7 +339,7 @@ export class QueueService {
//deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});
this.deliverQueue.clean(0, 'delayed');
this.inboxQueue.once('cleaned', (jobs, status) => {
//inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
});

View File

@@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
const FALLBACK = '❤';
@@ -60,9 +60,6 @@ export class ReactionService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -74,6 +71,7 @@ export class ReactionService {
private utilityService: UtilityService,
private metaService: MetaService,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
@@ -104,7 +102,6 @@ export class ReactionService {
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = '❤️';
} else {
// TODO: cache
reaction = await this.toDbReaction(reaction, user.host);
}
@@ -158,21 +155,22 @@ export class ReactionService {
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction);
// TODO: Cache
const emoji = await this.emojisRepository.findOne({
where: {
name: decodedReaction.name,
host: decodedReaction.host ?? IsNull(),
},
select: ['name', 'host', 'originalUrl', 'publicUrl'],
});
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
: await this.emojisRepository.findOne(
{
where: {
name: decodedReaction.name,
host: decodedReaction.host,
},
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction,
emoji: emoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl,
url: customEmoji.publicUrl || customEmoji.originalUrl,
} : null,
userId: user.id,
});
@@ -311,10 +309,12 @@ export class ReactionService {
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) {
const name = custom[1];
const emoji = await this.emojisRepository.findOneBy({
host: reacterHost ?? IsNull(),
name,
});
const emoji = reacterHost == null
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
: await this.emojisRepository.findOneBy({
host: reacterHost,
name,
});
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
}

View File

@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { MemoryCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable()
export class RelayService {
private relaysCache: MemoryCache<Relay[]>;
private relaysCache: MemorySingleCache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
}
@bindThis

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, MemoryCache } from '@/misc/cache.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@@ -13,6 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Packed } from '@/misc/json-schema';
import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
@@ -57,15 +58,18 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
private rolesCache: MemoryCache<Role[]>;
private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -84,10 +88,10 @@ export class RoleService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemoryCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@@ -398,8 +402,27 @@ export class RoleService implements OnApplicationShutdown {
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
}
@bindThis
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
const roles = await this.getUserRoles(note.userId);
const redisPipeline = this.redisClient.pipeline();
for (const role of roles) {
redisPipeline.xadd(
`roleTimeline:${role.id}`,
'MAXLEN', '~', '1000',
'*',
'note', note.id);
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
}
redisPipeline.exec();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@@ -24,7 +24,7 @@ export class UserBlockingService implements OnModuleInit {
constructor(
private moduleRef: ModuleRef,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -54,12 +54,12 @@ export class UserBlockingService implements OnModuleInit {
}
@bindThis
public async block(blocker: User, blockee: User) {
public async block(blocker: User, blockee: User, silent = false) {
await Promise.all([
this.cancelRequest(blocker, blockee),
this.cancelRequest(blockee, blocker),
this.userFollowingService.unfollow(blocker, blockee),
this.userFollowingService.unfollow(blockee, blocker),
this.cancelRequest(blocker, blockee, silent),
this.cancelRequest(blockee, blocker, silent),
this.userFollowingService.unfollow(blocker, blockee, silent),
this.userFollowingService.unfollow(blockee, blocker, silent),
this.removeFromList(blockee, blocker),
]);
@@ -89,7 +89,7 @@ export class UserBlockingService implements OnModuleInit {
}
@bindThis
private async cancelRequest(follower: User, followee: User) {
private async cancelRequest(follower: User, followee: User, silent = false) {
const request = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
@@ -110,7 +110,7 @@ export class UserBlockingService implements OnModuleInit {
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
if (this.userEntityService.isLocalUser(follower)) {
if (this.userEntityService.isLocalUser(follower) && !silent) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {

View File

@@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@@ -43,7 +44,10 @@ export class UserFollowingService implements OnModuleInit {
constructor(
private moduleRef: ModuleRef,
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -79,7 +83,7 @@ export class UserFollowingService implements OnModuleInit {
}
@bindThis
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise<void> {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }),
@@ -139,7 +143,7 @@ export class UserFollowingService implements OnModuleInit {
}
}
await this.insertFollowingDoc(followee, follower);
await this.insertFollowingDoc(followee, follower, silent);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
@@ -155,6 +159,7 @@ export class UserFollowingService implements OnModuleInit {
follower: {
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']
},
silent = false,
): Promise<void> {
if (follower.id === followee.id) return;
@@ -233,7 +238,7 @@ export class UserFollowingService implements OnModuleInit {
this.perUserFollowingChart.update(follower, followee, true);
// Publish follow event
if (this.userEntityService.isLocalUser(follower)) {
if (this.userEntityService.isLocalUser(follower) && !silent) {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
@@ -410,7 +415,7 @@ export class UserFollowingService implements OnModuleInit {
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
this.queueService.deliver(follower, content, followee.inbox, false);
}
}

View File

@@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class UserListService {
@@ -29,6 +30,7 @@ export class UserListService {
private roleService: RoleService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private queueService: QueueService,
) {
}
@@ -47,14 +49,14 @@ export class UserListService {
userId: target.id,
userListId: list.id,
} as UserListJoining);
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (this.userEntityService.isRemoteUser(target)) {
const proxy = await this.proxyAccountService.fetch();
if (proxy) {
this.userFollowingService.follow(proxy, target);
this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: target.id } }]);
}
}
}

View File

@@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
private webhooks: Webhook[] = [];
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository,
) {
//this.onMessage = this.onMessage.bind(this);
this.redisSubscriber.on('message', this.onMessage);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
@@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
this.redisForSub.off('message', this.onMessage);
}
}

View File

@@ -186,7 +186,7 @@ class DeliverManager {
for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.set(inbox, following.followerSharedInbox === null);
inboxes.set(inbox, following.followerSharedInbox != null);
}
}

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { In, IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -22,7 +22,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@@ -31,7 +31,7 @@ import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate } from './type.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
@Injectable()
export class ApInboxService {
@@ -80,7 +80,7 @@ export class ApInboxService {
) {
this.logger = this.apLoggerService.logger;
}
@bindThis
public async performActivity(actor: RemoteUser, activity: IObject) {
if (isCollectionOrOrderedCollection(activity)) {
@@ -139,6 +139,8 @@ export class ApInboxService {
await this.block(actor, activity);
} else if (isFlag(activity)) {
await this.flag(actor, activity);
} else if (isMove(activity)) {
//await this.move(actor, activity);
} else {
this.logger.warn(`unrecognized activity type: ${activity.type}`);
}
@@ -147,15 +149,15 @@ export class ApInboxService {
@bindThis
private async follow(actor: RemoteUser, activity: IFollow): Promise<string> {
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
if (followee == null) {
return 'skip: followee not found';
}
if (followee.host != null) {
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
}
await this.userFollowingService.follow(actor, followee, activity.id);
return 'ok';
}
@@ -183,16 +185,16 @@ export class ApInboxService {
const uri = activity.id ?? activity;
this.logger.info(`Accept: ${uri}`);
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(err => {
this.logger.error(`Resolution failed: ${err}`);
throw err;
});
if (isFollow(object)) return await this.acceptFollow(actor, object);
return `skip: Unknown Accept type: ${getApType(object)}`;
}
@@ -225,18 +227,18 @@ export class ApInboxService {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found');
await this.notePiningService.addPinned(actor, note.id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
}
@@ -405,10 +407,10 @@ export class ApInboxService {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
// 削除対象objectのtype
let formerType: string | undefined;
if (typeof activity.object === 'string') {
// typeが不明だけど、どうせ消えてるのでremote resolveしない
formerType = undefined;
@@ -420,19 +422,19 @@ export class ApInboxService {
formerType = toSingle(object.type);
}
}
const uri = getApId(activity.object);
// type不明でもactorとobjectが同じならばそれはPersonに違いない
if (!formerType && actor.uri === uri) {
formerType = 'Person';
}
// それでもなかったらおそらくNote
if (!formerType) {
formerType = 'Note';
}
if (validPost.includes(formerType)) {
return await this.deleteNote(actor, uri);
} else if (validActor.includes(formerType)) {
@@ -445,44 +447,44 @@ export class ApInboxService {
@bindThis
private async deleteActor(actor: RemoteUser, uri: string): Promise<string> {
this.logger.info(`Deleting the Actor: ${uri}`);
if (actor.uri !== uri) {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
const user = await this.usersRepository.findOneBy({ id: actor.id });
if (user == null) {
return 'skip: actor not found';
} else if (user.isDeleted) {
return 'skip: already deleted';
}
const job = await this.queueService.createDeleteAccountJob(actor);
await this.usersRepository.update(actor.id, {
isDeleted: true,
});
return `ok: queued ${job.name} ${job.id}`;
}
@bindThis
private async deleteNote(actor: RemoteUser, uri: string): Promise<string> {
this.logger.info(`Deleting the Note: ${uri}`);
const unlock = await this.appLockService.getApLock(uri);
try {
const note = await this.apDbResolverService.getNoteFromApId(uri);
if (note == null) {
return 'message not found';
}
if (note.userId !== actor.id) {
return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
}
await this.noteDeleteService.delete(actor, note);
return 'ok: note deleted';
} finally {
@@ -536,23 +538,23 @@ export class ApInboxService {
@bindThis
private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
if (follower == null) {
return 'skip: follower not found';
}
if (!this.userEntityService.isLocalUser(follower)) {
return 'skip: follower is not a local user';
}
// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await this.relayService.relayRejected(match[1]);
}
await this.userFollowingService.remoteReject(actor, follower);
return 'ok';
}
@@ -562,18 +564,18 @@ export class ApInboxService {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
if (activity.target == null) {
throw new Error('target is null');
}
if (activity.target === actor.featured) {
const note = await this.apNoteService.resolveNote(activity.object);
if (note == null) throw new Error('note not found');
await this.notePiningService.removePinned(actor, note.id);
return;
}
throw new Error(`unknown target: ${activity.target}`);
}
@@ -582,24 +584,24 @@ export class ApInboxService {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}
const uri = activity.id ?? activity;
this.logger.info(`Undo: ${uri}`);
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
throw e;
});
if (isFollow(object)) return await this.undoFollow(actor, object);
if (isBlock(object)) return await this.undoBlock(actor, object);
if (isLike(object)) return await this.undoLike(actor, object);
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
if (isAccept(object)) return await this.undoAccept(actor, object);
return `skip: unknown object type ${getApType(object)}`;
}
@@ -609,17 +611,17 @@ export class ApInboxService {
if (follower == null) {
return 'skip: follower not found';
}
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: actor.id,
});
if (following) {
await this.userFollowingService.unfollow(follower, actor);
return 'ok: unfollowed';
}
return 'skip: フォローされていない';
}
@@ -708,16 +710,16 @@ export class ApInboxService {
if ('actor' in activity && actor.uri !== activity.actor) {
return 'skip: invalid actor';
}
this.logger.debug('Update');
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
throw e;
});
if (isActor(object)) {
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
return 'ok: Person updated';
@@ -728,4 +730,59 @@ export class ApInboxService {
return `skip: Unknown type: ${getApType(object)}`;
}
}
@bindThis
private async move(actor: RemoteUser, activity: IMove): Promise<string> {
// fetch the new and old accounts
const targetUri = getApHrefNullable(activity.target);
if (!targetUri) return 'skip: invalid activity target';
let new_acc = await this.apPersonService.resolvePerson(targetUri);
let old_acc = await this.apPersonService.resolvePerson(actor.uri);
// update them if they're remote
if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
// retrieve updated users
new_acc = await this.apPersonService.resolvePerson(targetUri);
old_acc = await this.apPersonService.resolvePerson(actor.uri);
// check if alsoKnownAs of the new account is valid
let isValidMove = true;
if (old_acc.uri) {
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
isValidMove = false;
}
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
isValidMove = false;
}
if (!isValidMove) {
return 'skip: accounts invalid';
}
// add target uri to movedToUri in order to indicate that the user has moved
await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
// follow the new account and unfollow the old one
const followings = await this.followingsRepository.find({
relations: {
follower: true,
},
where: {
followeeId: old_acc.id,
followerHost: IsNull(), // follower is local
},
});
for (const following of followings) {
if (!following.follower) continue;
try {
await this.userFollowingService.follow(following.follower, new_acc);
await this.userFollowingService.unfollow(following.follower, old_acc);
} catch {
/* empty */
}
}
return 'ok';
}
}

View File

@@ -21,9 +21,11 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IIdentifier } from './models/identifier.js';
@Injectable()
@@ -50,6 +52,7 @@ export class ApRendererService {
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService,
@@ -272,11 +275,7 @@ export class ApRendererService {
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
// TODO: cache
const emoji = await this.emojisRepository.findOneBy({
name,
host: IsNull(),
});
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
if (emoji) object.tag = [this.renderEmoji(emoji)];
}
@@ -293,6 +292,22 @@ export class ApRendererService {
};
}
@bindThis
public renderMove(
src: { id: User['id']; host: User['host']; uri: User['host'] },
dst: { id: User['id']; host: User['host']; uri: User['host'] },
): IMove {
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor,
type: 'Move',
object: actor,
target,
};
}
@bindThis
public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => {
@@ -499,6 +514,14 @@ export class ApRendererService {
attachment: attachment.length ? attachment : undefined,
} as any;
if (user.movedToUri) {
person.movedTo = user.movedToUri;
}
if (user.alsoKnownAs) {
person.alsoKnownAs = user.alsoKnownAs;
}
if (profile.birthday) {
person['vcard:bday'] = profile.birthday;
}
@@ -701,13 +724,9 @@ export class ApRendererService {
private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
const emojis = await Promise.all(
names.map(name => this.emojisRepository.findOneBy({
name,
host: IsNull(),
})),
);
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
return emojis.filter(emoji => emoji != null) as Emoji[];
return emojis;
}
}

View File

@@ -1,5 +1,6 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -341,15 +342,17 @@ export class ApNoteService {
if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({
host,
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
});
return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon);
const exists = await this.emojisRepository.findOneBy({
host,
name,
});
const exists = existingEmojis.find(x => x.name === name);
if (exists) {
if ((tag.updated != null && exists.updatedAt == null)

View File

@@ -31,6 +31,7 @@ import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -49,6 +50,7 @@ const summaryLength = 2048;
export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService;
private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService;
private idService: IdService;
private globalEventService: GlobalEventService;
private metaService: MetaService;
@@ -113,6 +115,7 @@ export class ApPersonService implements OnModuleInit {
onModuleInit() {
this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService');
@@ -278,6 +281,8 @@ export class ApPersonService implements OnModuleInit {
lastFetchedAt: new Date(),
name: truncate(person.name, nameLength),
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
alsoKnownAs: person.alsoKnownAs,
isExplorable: !!person.discoverable,
username: person.preferredUsername,
usernameLower: person.preferredUsername!.toLowerCase(),
@@ -356,32 +361,44 @@ export class ApPersonService implements OnModuleInit {
const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user!.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarBlurhash,
bannerBlurhash,
});
user!.avatarId = avatarId;
user!.bannerId = bannerId;
//#endregion
user!.avatarId = avatarId;
user!.bannerId = bannerId;
user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[];
});
//#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.usersRepository.update(user!.id, {
emojis: emojiNames,
});
//#endregion
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user!;
return user!;
}
/**
@@ -458,15 +475,21 @@ export class ApPersonService implements OnModuleInit {
isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: !!person.discoverable,
} as Partial<User>;
if (avatar) {
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
}
if (banner) {
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
}
// Update user

View File

@@ -157,6 +157,8 @@ export interface IActor extends IObject {
name?: string;
preferredUsername?: string;
manuallyApprovesFollowers?: boolean;
movedTo?: string;
alsoKnownAs?: string[];
discoverable?: boolean;
inbox: string;
sharedInbox?: string; // 後方互換性のため
@@ -300,6 +302,11 @@ export interface IFlag extends IActivity {
type: 'Flag';
}
export interface IMove extends IActivity {
type: 'Move';
target: IObject | string;
}
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
@@ -314,3 +321,4 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';

View File

@@ -84,7 +84,7 @@ export class ChannelEntityService {
} : {}),
...(detailed ? {
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
pinnedNotes: (await this.noteEntityService.packMany(pinnedNotes, me)).sort((a, b) => channel.pinnedNoteIds.indexOf(a.id) - channel.pinnedNoteIds.indexOf(b.id)),
} : {}),
};
}

View File

@@ -266,6 +266,7 @@ export class DriveFileEntityService {
fileIds: DriveFile['id'][],
options?: PackOptions,
): Promise<Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>> {
if (fileIds.length === 0) return new Map();
const files = await this.driveFilesRepository.findBy({ id: In(fileIds) });
const packedFiles = await this.packMany(files, options);
const map = new Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>(packedFiles.map(f => [f.id, f]));
@@ -280,6 +281,7 @@ export class DriveFileEntityService {
fileIds: DriveFile['id'][],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
if (fileIds.length === 0) return [];
const filesMap = await this.packManyByIdsMap(fileIds, options);
return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
}

View File

@@ -183,6 +183,11 @@ export class NoteEntityService implements OnModuleInit {
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
// パフォーマンスのためートが作成されてから1秒以上経っていない場合はリアクションを取得しない
if (note.createdAt.getTime() + 1000 > Date.now()) {
return undefined;
}
const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId,
noteId: note.id,
@@ -283,7 +288,7 @@ export class NoteEntityService implements OnModuleInit {
}, options);
const meId = me ? me.id : null;
const note = typeof src === 'object' ? src : await this.notesRepository.findOneByOrFail({ id: src });
const note = typeof src === 'object' ? src : await this.notesRepository.findOneOrFail({ where: { id: src }, relations: ['user'] });
const host = note.userHost;
let text = note.text;
@@ -395,7 +400,8 @@ export class NoteEntityService implements OnModuleInit {
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...notes.map(n => n.id), ...renoteIds];
// パフォーマンスのためートが作成されてから1秒以上経っていない場合はリアクションを取得しない
const targets = [...notes.filter(n => n.createdAt.getTime() + 1000 < Date.now()).map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
@@ -406,10 +412,10 @@ export class NoteEntityService implements OnModuleInit {
}
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
@@ -420,6 +426,30 @@ export class NoteEntityService implements OnModuleInit {
})));
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
@bindThis
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える

View File

@@ -108,27 +108,30 @@ export class NotificationEntityService implements OnModuleInit {
) {
if (notifications.length === 0) return [];
const noteIds = notifications.map(x => x.noteId).filter(isNotNull);
let validNotifications = notifications;
const noteIds = validNotifications.map(x => x.noteId).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'user.avatar', 'user.banner', 'reply', 'reply.user', 'reply.user.avatar', 'reply.user.banner', 'renote', 'renote.user', 'renote.user.avatar', 'renote.user.banner'],
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
}) : [];
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
validNotifications = validNotifications.filter(x => x.noteId == null || packedNotes.has(x.noteId));
const userIds = validNotifications.map(x => x.notifierId).filter(isNotNull);
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
relations: ['avatar', 'banner'],
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
detail: false,
});
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
return await Promise.all(notifications.map(x => this.pack(x, meId, {}, {
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
packedNotes,
packedUsers,
})));

View File

@@ -9,13 +9,14 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { AntennaService } from '../AntennaService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
@@ -25,7 +26,7 @@ import type { PageEntityService } from './PageEntityService.js';
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
Detailed extends true ?
Detailed extends true ?
ExpectsMe extends true ? Packed<'MeDetailed'> :
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
Packed<'UserDetailed'> :
@@ -47,13 +48,14 @@ function isRemoteUser(user: User | { host: User['host'] }): boolean {
@Injectable()
export class UserEntityService implements OnModuleInit {
private apPersonService: ApPersonService;
private noteEntityService: NoteEntityService;
private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private antennaService: AntennaService;
private roleService: RoleService;
private userInstanceCache: MemoryKVCache<Instance | null>;
private federatedInstanceService: FederatedInstanceService;
constructor(
private moduleRef: ModuleRef,
@@ -119,16 +121,17 @@ export class UserEntityService implements OnModuleInit {
//private antennaService: AntennaService,
//private roleService: RoleService,
) {
this.userInstanceCache = new MemoryKVCache<Instance | null>(1000 * 60 * 60 * 3);
}
onModuleInit() {
this.apPersonService = this.moduleRef.get('ApPersonService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.antennaService = this.moduleRef.get('AntennaService');
this.roleService = this.moduleRef.get('RoleService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
}
//#region Validators
@@ -237,7 +240,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
@@ -269,27 +272,6 @@ export class UserEntityService implements OnModuleInit {
);
}
@bindThis
public async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else if (user.avatarId) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else {
return this.getIdenticonUrl(user);
}
}
@bindThis
public getAvatarUrlSync(user: User): string {
if (user.avatar) {
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
} else {
return this.getIdenticonUrl(user);
}
}
@bindThis
public getIdenticonUrl(user: User): string {
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
@@ -309,19 +291,23 @@ export class UserEntityService implements OnModuleInit {
includeSecrets: false,
}, options);
let user: User;
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
if (typeof src === 'object') {
user = src;
if (src.avatar === undefined && src.avatarId) src.avatar = await this.driveFilesRepository.findOneBy({ id: src.avatarId }) ?? null;
if (src.banner === undefined && src.bannerId) src.banner = await this.driveFilesRepository.findOneBy({ id: src.bannerId }) ?? null;
} else {
user = await this.usersRepository.findOneOrFail({
where: { id: src },
relations: {
avatar: true,
banner: true,
},
// migration
if (user.avatarId != null && user.avatarUrl === null) {
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
this.usersRepository.update(user.id, {
avatarUrl: user.avatarUrl,
avatarBlurhash: avatar.blurhash,
});
}
if (user.bannerId != null && user.bannerUrl === null) {
const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
this.usersRepository.update(user.id, {
bannerUrl: user.bannerUrl,
bannerBlurhash: banner.blurhash,
});
}
@@ -356,14 +342,11 @@ export class UserEntityService implements OnModuleInit {
name: user.name,
username: user.username,
host: user.host,
avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash ?? null,
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
avatarBlurhash: user.avatarBlurhash,
isBot: user.isBot ?? falsy,
isCat: user.isCat ?? falsy,
instance: user.host ? this.userInstanceCache.fetch(user.host,
() => this.instancesRepository.findOneBy({ host: user.host! }),
v => v != null,
).then(instance => instance ? {
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
@@ -383,11 +366,13 @@ export class UserEntityService implements OnModuleInit {
...(opts.detail ? {
url: profile!.url,
uri: user.uri,
movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null,
alsoKnownAs: user.alsoKnownAs,
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) : null,
bannerBlurhash: user.banner?.blurhash ?? null,
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended ?? falsy,

View File

@@ -2,7 +2,8 @@ export const DI = {
config: Symbol('config'),
db: Symbol('db'),
redis: Symbol('redis'),
redisSubscriber: Symbol('redisSubscriber'),
redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'),
//#region Repositories
usersRepository: Symbol('usersRepository'),

View File

@@ -8,7 +8,7 @@ export class RedisKVCache<T> {
private memoryCache: MemoryKVCache<T>;
private fetcher: (key: string) => Promise<T>;
private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T;
private fromRedisConverter: (value: string) => T | undefined;
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
lifetime: RedisKVCache<T>['lifetime'];
@@ -85,6 +85,90 @@ export class RedisKVCache<T> {
}
}
export class RedisSingleCache<T> {
private redisClient: Redis.Redis;
private name: string;
private lifetime: number;
private memoryCache: MemorySingleCache<T>;
private fetcher: () => Promise<T>;
private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T | undefined;
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
lifetime: RedisSingleCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisSingleCache<T>['fetcher'];
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
}) {
this.redisClient = redisClient;
this.name = name;
this.lifetime = opts.lifetime;
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
this.toRedisConverter = opts.toRedisConverter;
this.fromRedisConverter = opts.fromRedisConverter;
}
@bindThis
public async set(value: T): Promise<void> {
this.memoryCache.set(value);
if (this.lifetime === Infinity) {
await this.redisClient.set(
`singlecache:${this.name}`,
this.toRedisConverter(value),
);
} else {
await this.redisClient.set(
`singlecache:${this.name}`,
this.toRedisConverter(value),
'ex', Math.round(this.lifetime / 1000),
);
}
}
@bindThis
public async get(): Promise<T | undefined> {
const memoryCached = this.memoryCache.get();
if (memoryCached !== undefined) return memoryCached;
const cached = await this.redisClient.get(`singlecache:${this.name}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
}
@bindThis
public async delete(): Promise<void> {
this.memoryCache.delete();
await this.redisClient.del(`singlecache:${this.name}`);
}
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
*/
@bindThis
public async fetch(): Promise<T> {
const cachedValue = await this.get();
if (cachedValue !== undefined) {
// Cache HIT
return cachedValue;
}
// Cache MISS
const value = await this.fetcher();
this.set(value);
return value;
}
@bindThis
public async refresh() {
const value = await this.fetcher();
this.set(value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
}
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class MemoryKVCache<T> {
@@ -173,12 +257,12 @@ export class MemoryKVCache<T> {
}
}
export class MemoryCache<T> {
export class MemorySingleCache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: MemoryCache<never>['lifetime']) {
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
this.lifetime = lifetime;
}

View File

@@ -3,6 +3,8 @@
import * as crypto from 'node:crypto';
export const aidRegExp = /^[0-9a-z]{10}$/;
const TIME2000 = 946684800000;
let counter = crypto.randomBytes(2).readUInt16LE(0);

View File

@@ -1,5 +1,8 @@
const CHARS = '0123456789abcdef';
// same as object-id
export const meidRegExp = /^[0-9a-f]{24}$/;
function getTime(time: number) {
if (time < 0) time = 0;
if (time === 0) {
@@ -24,3 +27,9 @@ function getRandom() {
export function genMeid(date: Date): string {
return getTime(date.getTime()) + getRandom();
}
export function parseMeid(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000),
};
}

View File

@@ -3,6 +3,7 @@ const CHARS = '0123456789abcdef';
// 4bit Fixed hex value 'g'
// 44bit UNIX Time ms in Hex
// 48bit Random value in Hex
export const meidgRegExp = /^g[0-9a-f]{23}$/;
function getTime(time: number) {
if (time < 0) time = 0;
@@ -26,3 +27,9 @@ function getRandom() {
export function genMeidg(date: Date): string {
return 'g' + getTime(date.getTime()) + getRandom();
}
export function parseMeidg(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(1, 12), 16)),
};
}

View File

@@ -1,5 +1,8 @@
const CHARS = '0123456789abcdef';
// same as meid
export const objectIdRegExp = /^[0-9a-f]{24}$/;
function getTime(time: number) {
if (time < 0) time = 0;
if (time === 0) {
@@ -24,3 +27,9 @@ function getRandom() {
export function genObjectId(date: Date): string {
return getTime(date.getTime()) + getRandom();
}
export function parseObjectId(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(0, 8), 16) * 1000),
};
}

View File

@@ -0,0 +1,14 @@
// Crockford's Base32
// https://github.com/ulid/spec#encoding
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
export function parseUlid(id: string): { date: Date; } {
const timestamp = id.slice(0, 10);
let time = 0;
for (let i = 0; i < 10; i++) {
time = time * 32 + CHARS.indexOf(timestamp[i]);
}
return { date: new Date(time) };
}

View File

@@ -68,6 +68,19 @@ export class User {
})
public followingCount: number;
@Column('varchar', {
length: 512,
nullable: true,
comment: 'The URI of the new account of the User',
})
public movedToUri: string | null;
@Column('simple-array', {
nullable: true,
comment: 'URIs the user is known as too',
})
public alsoKnownAs: string[] | null;
@Column('integer', {
default: 0,
comment: 'The count of notes.',
@@ -100,6 +113,26 @@ export class User {
@JoinColumn()
public banner: DriveFile | null;
@Column('varchar', {
length: 512, nullable: true,
})
public avatarUrl: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public bannerUrl: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public avatarBlurhash: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public bannerBlurhash: string | null;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}',

View File

@@ -72,6 +72,18 @@ export const packedUserDetailedNotMeOnlySchema = {
format: 'uri',
nullable: true, optional: false,
},
movedToUri: {
type: 'string',
format: 'uri',
nullable: true,
optional: false,
},
alsoKnownAs: {
type: 'array',
format: 'uri',
nullable: true,
optional: false,
},
createdAt: {
type: 'string',
nullable: false, optional: false,

View File

@@ -200,6 +200,22 @@ export function createPostgresDataSource(config: Config) {
statement_timeout: 1000 * 10,
...config.db.extra,
},
replication: config.dbReplications ? {
master: {
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
},
slaves: config.dbSlaves!.map(rep => ({
host: rep.host,
port: rep.port,
username: rep.user,
password: rep.pass,
database: rep.db,
})),
} : undefined,
synchronize: process.env.NODE_ENV === 'test',
dropSchema: process.env.NODE_ENV === 'test',
cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...)

View File

@@ -52,8 +52,10 @@ export class DbQueueProcessorsService {
q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done));
q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));

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