Compare commits

...

108 Commits

Author SHA1 Message Date
まっちゃとーにゅ c7be019fdd fix(api/users): ページビューの並び替えが正常に計算されない問題を修正 (MisskeyIO#728)
(cherry picked from commit 1400dbfd2a0c6f88f9df2f6a3a5236f78dc13f14)
2024-09-16 22:29:12 +09:00
kakkokari-gtyih e898f98b34 Merge branch 'develop' into feat-14194 2024-09-16 22:09:53 +09:00
かっこかり 7d7a12d7d6
fix(deps): broken lockfile (#14556) 2024-09-15 21:57:22 +09:00
dependabot[bot] 887c709647
chore(deps): bump body-parser from 1.20.2 to 1.20.3 in /packages/backend (#14550)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-15 20:54:26 +09:00
かっこかり 0e4b6d1dad
enhance(frontend): adminのファイルリストでセンシティブファイルに枠線を追加 (#14510)
* enhance(frontend): adminのファイルリストでセンシティブファイルに枠線を追加

* Update Changelog
2024-09-15 17:50:25 +09:00
Juan Aguilar Santillana 07f26bc8dd
refactor(backend): use Reflet for autobind deco (#14482)
Using Reflect.defineProperty instead of Object.defineProperty
gives a more consistent behavior with the rest of the modern
JavaScript features.
2024-09-15 17:43:24 +09:00
syuilo 366b79e459
Update CHANGELOG.md 2024-09-15 15:14:13 +09:00
Kisaragi 6b2072f4b1
fix(backend/antenna): キーワードが与えられなかった場合のエラーをApiErrorとして投げる (#14491)
* fix(backend/antenna): report validation failure as ApiError on update

* test(backend/antenna): reflect change in previous commit

* fix(backend/antenna): report validation failure as ApiError on create

* test(backend/antenna): reflect change in previous commit

* test(backend/antenna): semi

* test(backend/antenna): bring being spread parameters first in object literal

* chore: add CHANGELOG entry

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-09-15 15:13:46 +09:00
かっこかり 1544ba9153
refactor(frontend): 非推奨の表現を改める (#14517) 2024-09-15 12:31:17 +09:00
かっこかり be0906a6c7
fix(backend): happy-domで外部HTMLをパースする際に関連リソースが読み込まれる問題を修正 (#14521)
* bump happy-dom, disable all JS&c when parsing

version 10 didn't quite support disabling all of that

I have tested that `MfmService` (the other code that uses `happy-dom`)
still works fine: the RSS feed for a user is generated correctly, with
HTML rendered from MFM

(cherry picked from commit 26e0412fbb91447c37e8fb06ffb0487346063bb8)

* Update Changelog

* lint

* fix possible memory leak

---------

Co-authored-by: dakkar <dakkar@thenautilus.net>
2024-09-15 12:30:27 +09:00
かっこかり e0f54d6a68
fix(frontend): MkDateSeparatedListで月の違う同じ日はセパレータが出ないのを修正 (#14545)
* fix(frontend): MkDateSeparatedListで月の違う同じ日はセパレータが出ないのを修正

* Update Changelog
2024-09-15 12:20:29 +09:00
かっこかり 837a8e15d8
refactor(frontend): frontend-embed/src/to-be-sharedを共通化 (#14536)
* refactor(frontend): shouldCollapsedを共通化

* refactor(frontend): config.js, worker-multi-dispatch.js, intl-const.jsを共通化

* fix(frontend-shared): fix type error

* refactor(frontend): is-link.jsと、同一の振る舞いをする記述を共通化

* fix

* fix lint

* lint fixes
2024-09-10 18:39:53 +09:00
KanariKanaru 0c2cfe31a3
Dev: cypressをdev containerで実行可に(e2e-dev-container) (#14526) 2024-09-10 16:33:14 +09:00
かっこかり 05c944c2cc
Update CHANGELOG.md (書き方を揃える) 2024-09-10 16:25:36 +09:00
かっこかり f393b6b898
fix(frontend/frontend-embed): インポートパス・テーマまわりなどの修正 (#14535)
* fix(frontend/frontend-embed): wrong imports

* enhance(frontend-embed): サーバーデフォルトのテーマがある場合はそちらを利用するように

* 🎨

* 🎨

* 🎨
2024-09-10 16:14:02 +09:00
かっこかり 672779a15f
fix(frontend-embed): 不足していたスタイル・インポートを追加 (#14531)
* fix(frontend-embed): add missing imports

* fix(frontend-embed): add missing styles
2024-09-09 22:44:39 +09:00
かっこかり 2cbe1d1210
feat(frontend): ノート・ユーザータイムライン埋め込み (#13929)
* fix

* navhookをbootに移動

* サーバーサイドのbootも分けるように

* 埋め込みページかどうかの判定は最初の一回だけに

* tooltipは出せるように

* fix design

* 埋め込み独自のtooltipを削除

* ロジックの分岐が多かったMkNoteDetailedを分離

* fix indent

* プレビュー用iframeにフォーカスが当たるのを修正

* popupの制御を出す側で行うように

* パラメータが逆になっていたのを修正

* Update MkEmbedCodeGenDialog.vue

* fix

* eliminate misskey-js lint warns

* fix

* add appropriate attributes to embed html

* enhance: サーバーサイドのembed系をさらに分離

* enhance: embed routerを分離(route定義をboot時に変更できるようにする改修を含む)

* type

* lint

* fix indent

* server-side styleを完全に分離

* Revert "refactor: 画面サイズのしきい値をconstにまとめる"

This reverts commit 05ca36f400.

* fix

* revert all changes in base.pug

* embedドメインをまとめた

* embedドメインをまとめた

* prevent calling contextmenu in embed page by stopping at the caller

* fix import

* fix import

* improve directory structure

* fix import

* register timeline ui as a container

* wa-

* rename

* wa-

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaImage.vue

* Update EmNote.vue

* revert mkmedialist changes

* 戻し漏れ

* wip

* tweak embed media ui

* revert original media components

* Update boot.embed.js

* rename

* wip

* Update MkNote.vue

* wip

* Update MkSubNoteContent.vue

* Update EmNote.vue

* Update packages/frontend/src/router/definition.ts

* Revert "Update packages/frontend/src/router/definition.ts"

This reverts commit 937ae44521.

* refactor EmMediaImage

* fix import

* remove unused imports

* Update router.ts

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* Update EmNote.vue

* Update EmNote.vue

* Create EmA.vue

* Create EmAvatar.vue

* Update EmAvatar.vue

* wip

* wip

* wip

* Create EmImgWithBlurhash.vue

* Update EmImgWithBlurhash.vue

* Create EmPagination.vue

* wip

* Update boot.ts

* wip

* wip

* wi@p

* wip

* wip

* wiop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update boot.ts

* wip

* Update MkMisskeyFlavoredMarkdown.ts

* wip

* wip

* wip

* wip

* wip

* Update post-message.ts

* wip

* Update EmNoteDetailed.vue

* Update EmNoteDetailed.vue

* Create instance.ts

* Update EmNoteDetailed.vue

* wip

* Update EmNoteDetailed.vue

* wip

* wip

* wip

* Update pnpm-lock.yaml

* wip

* wip

* wp

* wip

* Update ClientServerService.ts

* wip

* Update boot.ts

* Update vite.config.local-dev.ts

* Update vite.config.ts

* Create index.html

* wa-

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Create EmLink.vue

* Create EmMention.vue

* Update EmMfm.ts

* wip

* wip

* wip

* wip

* Update vite.config.ts

* Update boot.ts

* Update EmA.vue

* うぃp

* wip

* wip

* Create EmError.vue

* wip

* Update MkEmbedCodeGenDialog.vue

* Update EmNote.vue

* wip

* wip

* Update user-timeline.vue

* Update check-spdx-license-id.yml

* wip

* wip

* style(frontend-shared): lint fixes on build.js

* fix(frontend-shared): include `*.{js,json}` files in js-built

* wip

* use alias

* refactor

* refactor

* Update scroll.ts

* refactor

* refactor

* refactor

* wip

* wip

* wip

* wip

* Update roles.vue

* Update branding.vue

* wip

* wip

* wip

* Update page.vue

* wip

* fix import

* add missing css variables

* 絵文字をtwemojiに変更

クライアントデフォルトにあわせるため

* force empoll readonly

* fix compiler error

* fix broken imports

* tweak button style

* run api extractor

* fix storybook theme preloads

* fix storybook instance imports

* Update preview.ts

* Update preview.ts

* Update preview.ts

* Revert "Update preview.ts"

This reverts commit 12bab1c6fb.

* Revert "Update preview.ts"

This reverts commit 5c0ce01dbd.

* Revert "Update preview.ts"

This reverts commit f4863524d7.

* Revert "fix storybook instance imports"

This reverts commit ed8eabb246.

* Revert "wip"

This reverts commit d3c1926519.

* Revert "Update page.vue"

This reverts commit 27c7900b0c.

* Revert "Update branding.vue"

This reverts commit c08ccb65ba.

* Revert "Update roles.vue"

This reverts commit 1488b67066.

* Revert "wip"

This reverts commit aab1c76981.

* refactor: use common media proxy

* fix imports

* fix

* fix: MediaProxyの初期化を保証する(storybook対策?)

* enhance(frontend-embed): improve embedParams provide

* fix(backend): MK_DEV_PREFER=backendのときにembed viteが読み込めないのを修正

* fix

* embed-pageを共通化

* fix import

* fix import

* fix import

* const.jsを共通化

(たぶんrevertしすぎた)

* fix type error

* fix duplicated import

* fix lint

* fix

* コメントとして残す

* sharedとembedをlint対象にする

* lint

* attempt to fix eslint (frontend-shared)

* lint fixes

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2024-09-09 20:57:36 +09:00
かっこかり 0d0cd738f8
refactor(misskey-js): warnを除去 (#14520) 2024-09-07 02:38:01 +09:00
かっこかり 567acea2a3
fix(frontend): instance infoページで不必要なapiリクエストが飛ぶのを抑止 (#14515)
* fix(frontend): instance infoページで不必要なapiリクエストが飛ぶのを抑止

* fix
2024-09-06 17:23:40 +09:00
かっこかり 8d19bdbb65
fix(misskey-js): content-typeはapplication/jsonでないもののみを記録するように (#14508) 2024-09-06 17:22:45 +09:00
かっこかり cdb0566c5b
refactor(frontend): scss deprecated 警告に対応 (#14513) 2024-09-06 16:12:14 +09:00
かっこかり f7398faeac
enhance(frontend): アイコンデコレーション管理画面にプレビューを追加 (#14511)
* enhance(frontend): アイコンデコレーション管理画面にプレビューを追加

* Update Changelog

* tweak
2024-09-06 15:37:03 +09:00
taiy c8f49b6ae7
chore(ci/lint): ESLintのキャッシュが保存できない問題を修正 (#14506) 2024-09-06 14:45:53 +09:00
syuilo 74c93fcebe Update .gitignore 2024-09-05 14:07:36 +09:00
zyoshoka 8be624aa44
refactor(sw): fix type errors (#14478)
* style(sw): lint fixes

* refactor(sw): fix type errors

* chore(sw): disable `noImplicitAny`

* ci(sw): enable typecheck ci

* ci(sw): build `misskey-js` before typecheck
2024-08-30 15:53:04 +09:00
zyoshoka 3fe7e37f10
fix(frontend): server metrics look strange after reload (#14467) 2024-08-30 10:59:23 +09:00
zyoshoka 7fe3035059
fix(backend): use `prefixItems` in `admin/queue/*-delayed` endpoint schema (#14468)
* fix(backend): represent tuples with `prefixItems`

* refactor(frontend): fix type errors

* fix(backend): add `prefixItems` in `SchemaType`

* fix(backend): add `unevaluatedItems: false` to disallow extra items

* refactor(frontend): consolidate `'deliver' | 'queue'` type def into `queue.vue`

* fix(backend): add `unevaluatedItems` in `SchemaType`
2024-08-30 10:58:59 +09:00
zyoshoka 06855f769f
refactor(sw): use fully typed locales (#14470)
* refactor(sw): use fully typed locales

* fix(backend): enumerate achievement notification type
2024-08-30 10:58:11 +09:00
zyoshoka 3e85052754
fix(backend): correct `app`-type notification schema (#14471) 2024-08-29 18:57:44 +09:00
syuilo b6fdd71957 消し忘れ 2024-08-27 20:40:11 +09:00
syuilo 36dff66883 refactor 2024-08-27 20:36:43 +09:00
Kisaragi 255c8bd1b9
fix: 投稿フォームの字数上限計算を実際の投稿内容に合わせる (#14466) 2024-08-26 15:55:38 +09:00
syuilo 44f62160cb enhance(frontend): error message i18n 2024-08-24 16:59:17 +09:00
syuilo 8032a4e12a enhance(frontend): サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように 2024-08-23 16:40:11 +09:00
syuilo 2f009f7d49 add note 2024-08-23 16:31:16 +09:00
syuilo f85aa7b641 fix(backend): ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 2024-08-23 16:29:05 +09:00
syuilo 1008fa32a0
better boot error screen 2024-08-22 14:03:11 +09:00
atsuchan 043ab1f69b
fix(backend): Fix chart generation non-matching blockedHosts (#14441) 2024-08-20 13:49:42 +09:00
かっこかり 21a3095eb0
fix button translation (#14444)
that Japanese string exactly matches that i18n key

(cherry picked from commit a408d32bb72ada9a4ad6bd1afe6e3fadb9b403db)

Co-authored-by: dakkar <dakkar@thenautilus.net>
2024-08-20 12:39:01 +09:00
syuilo 1b5f0571f7 🎨 2024-08-20 10:51:02 +09:00
syuilo 59e83605ac refactor 2024-08-19 19:41:06 +09:00
syuilo 130ff361c3 refactor 2024-08-19 17:32:27 +09:00
syuilo e78110a5cd refactor 2024-08-19 13:13:32 +09:00
github-actions[bot] 6c5593d456 [skip ci] Update CHANGELOG.md (prepend template) 2024-08-18 08:08:49 +00:00
github-actions[bot] 621626aad3 Release: 2024.8.0 2024-08-18 08:08:38 +00:00
syuilo f4f55ef012
New translations ja-jp.yml (English) (#14432) 2024-08-18 16:03:07 +09:00
github-actions[bot] 2e8a1029a4 Bump version to 2024.8.0-rc.5 2024-08-18 05:21:24 +00:00
かっこかり b53ee54e4f
fix(frontend): focustrapがzindexを考慮するように (#14431) 2024-08-18 14:18:46 +09:00
github-actions[bot] b708b27bc8 Bump version to 2024.8.0-rc.4 2024-08-18 04:37:19 +00:00
Hazel K 9ce44b24b8
fix(backend): memory leak in memory caches (#14363)
* encapsulate `MemoryKVCache<T>`

* remove infinity caches

* encapsulate other caches

* add missing awaits to internally synchronize caches

* implement pull-through caching

* tune cache lifetimes

* optimize cache GC by stopping early

* summarize changes in CHANGELOG.md

* Fix timeout comments

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* add comments about awaiting the redis write

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-08-18 13:34:01 +09:00
syuilo 3cd5f86510 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-08-18 10:12:51 +09:00
syuilo 9b78ce8047 🎨 2024-08-18 10:12:42 +09:00
syuilo 1629c0e50d
New Crowdin updates (#14426)
* New translations ja-jp.yml (Chinese Traditional)

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

* New translations ja-jp.yml (Chinese Simplified)
2024-08-18 10:11:50 +09:00
syuilo 427f4a2cda Update about-misskey.vue 2024-08-18 10:10:06 +09:00
woxtu ba9c5c37b8
Remove undefined style (#14427) 2024-08-18 03:40:21 +09:00
syuilo e790aa0548
[skip ci] New Crowdin updates (#14423)
* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)
2024-08-17 20:25:15 +09:00
taichan bf8c42eecd
Fix(beckend): html content-type detection on signedGet (#14424)
* fix(backend): contenttype detection of html in signedGet

* code style

* fix by review
2024-08-17 19:51:56 +09:00
かっこかり 129af06198
Update packages/backend/src/core/activitypub/ApRequestService.ts 2024-08-17 18:25:46 +09:00
かっこかり 83c04c55ad
fix(misskey-js): eliminate any (follow-up of #13523) (#14422) 2024-08-17 18:15:46 +09:00
かっこかり 0b98554319
Update CHANGELOG.md
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
2024-08-17 18:05:29 +09:00
かっこかり 4e0d57000c
fix(frontend): 以前のpopupの呼び出し方を修正 (#14421) 2024-08-17 18:04:52 +09:00
syuilo c0de57c08d Update about-misskey.vue 2024-08-17 17:57:27 +09:00
かっこかり 75b0315ace
Update timeline.vue (refactor) 2024-08-17 15:24:29 +09:00
github-actions[bot] 6cdecd72ee Bump version to 2024.8.0-rc.3 2024-08-17 06:17:02 +00:00
taichan 9fbc1b7f7b
enhance(backend): headタグ内にrel=alternateの指定のあるlinkタグがある場合、記述されたURLを参照して照会できるように (#14371)
* signedGet時にhttpかつalternate属性のlinkがある場合に一回だけfollowして照会する

* Fix: validation position

* Fix import

* Fix tagname

* Update CHANGELOG

* Fix code style

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-08-17 15:12:23 +09:00
zyoshoka fd744f44c1
enhance(backend): ページ、ギャラリー、Playのモデレーション強化 (#13523)
* enhance(backend): Page、ギャラリー、Playのモデレーション強化

* Update CHANGELOG.md

* fix: update misskey-js

* refactor(frontend): use `MkA`

* Update CHANGELOG.md

* fix(i18n): Page -> ページ
2024-08-17 15:01:08 +09:00
syuilo 383c41bdb6 🎨 2024-08-17 14:57:26 +09:00
github-actions[bot] 68ec7450af Bump version to 2024.8.0-beta.2 2024-08-17 04:11:09 +00:00
かっこかり 06684fe49b
fix(backend): ベースロールのポリシー変更時モデレーションログが残るように (#14418)
* fix(policies): ベースロールのポリシー変更時モデレーションログが残るように (MisskeyIO#700)

(cherry picked from commit 80389a914049f6f26237fde8da7d4e1bd41452fc)

* Update Changelog

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-08-17 12:07:00 +09:00
かっこかり 059eb6d379
fix(frontend): リノートの判定が甘いのを修正 (#14396)
* fix(frontend): リノートの判定が甘いのを修正

* fix

* Update Changelog

* fix

* use type assertion

* fix + add comments

* lint

* misskey-jsに移動

* PureRenote -> Renote

* isRenote -> isPureRenote
2024-08-17 11:28:22 +09:00
syuilo 61cc3b5642
New Crowdin updates (#14393)
* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Spanish)
2024-08-17 10:35:41 +09:00
github-actions[bot] 2ab5ee81b1 Bump version to 2024.8.0-alpha.1 2024-08-17 01:25:43 +00:00
syuilo ef950a345b
suspend周りの改修 (#14409)
* enhance(backend): 凍結されたアカウントのフォローリクエストを表示しないように

* Update CHANGELOG.md

* wip

* Update gen-spec.ts

* Update packages/backend/src/server/api/endpoints/admin/suspend-user.ts

Co-authored-by: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>

* owa-

* revert misskey-js related changes (#14414)

---------

Co-authored-by: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Co-authored-by: anatawa12 <anatawa12@icloud.com>
2024-08-17 09:57:28 +09:00
syuilo bfaf938609 update misskey-dev/eslint-plugin 2024-08-17 09:38:16 +09:00
syuilo d3cdc08802 fix(backend): 無制限にストリーミングのチャンネルに接続できる問題を修正 2024-08-17 09:30:03 +09:00
かっこかり 571566d476
Update timeline.vue (fix wrong type) 2024-08-16 22:01:01 +09:00
anatawa12 748a7e8f6a
feat: localizable dialog title for enter section title (#14401)
* feat: localizable dialog title for enter section title

* refactor: define `getPageBlockList` in separated file and import instead of provide/inject
2024-08-16 21:47:44 +09:00
かっこかり 6db3c50e32
fix(frontend): Misskeyの利用中に使用できるタイムラインの種類が変化した場合、使用できないタイムラインが表示されないように (#14412)
* fix(frontend): Misskeyの利用中に使用できるタイムラインの種類が変化した場合、使用できないタイムラインが表示されないように

* Update timeline.vue

* Update timeline.vue
2024-08-16 21:43:20 +09:00
zyoshoka 26322048db
fix(frontend): correct condition to displayed as system account on moderation page (#14407) 2024-08-16 21:04:08 +09:00
かっこかり a8810af8d9
fix(backend): リバーシの設定変更が反映されないのを修正 (#14404)
* fix(backend): リバーシの設定変更が反映されないのを修正

* Update Changelog

* add bindthis
2024-08-16 21:02:12 +09:00
syuilo 45d88574c3 enhance(frontend): improve usability 2024-08-15 20:02:53 +09:00
zyoshoka b68b2ee8c6
refactor(frontend): remove stale reload method call on `/admin/users` page (#14406) 2024-08-14 20:29:33 +09:00
syuilo 86dd4abadc Revert "enhance(backend): 凍結されたアカウントのフォローリクエストを表示しないように"
This reverts commit cd210001e6.
2024-08-14 20:29:06 +09:00
syuilo cd210001e6 enhance(backend): 凍結されたアカウントのフォローリクエストを表示しないように 2024-08-14 18:08:51 +09:00
timesince 41936c16c4
chore: fix some comments (#14394)
Signed-off-by: timesince <seekseat@icloud.com>
2024-08-12 14:03:16 +09:00
github-actions[bot] 4d757865f4 Bump version to 2024.8.0-alpha.0 2024-08-11 07:59:20 +00:00
syuilo 2a2bbcd1bc
New Crowdin updates (#14341)
* New translations ja-jp.yml (Thai)

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

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

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

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Thai)

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

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

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Portuguese)
2024-08-11 16:27:42 +09:00
shika 94b8c00c66
docker-compose.yml, compose.ymlを追跡対象外に追加 (#14345) 2024-08-11 16:27:24 +09:00
かっこかり ab7bbd4e57
fix(backend): ダイレクト投稿をユーザーごとのチャートから除外 (#14350)
* fix(chart): ダイレクト投稿をユーザーごとのチャートから除外 (MisskeyIO#679)

(cherry picked from commit 3db41c2d829ac18daabbdf52fe6235a874735b31)

* Update Changelog

---------

Co-authored-by: Yuuki <yukikum57@gmail.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-08-11 16:27:08 +09:00
かっこかり 93fc06d18b
fix(backend): getApTypeでエラーを投げないように (#14361)
* fix(backend): getApTypeでエラーを投げないように

* Update Changelog

* Update Changelog

* Update type.ts

* add comment
2024-08-11 16:25:57 +09:00
かっこかり 0aaf74ee22
fix(backend): InstanceEntityService.packMany に me が渡っていないのを修正 (#14360)
* fix: pass current user into `InstanceEntityService.packMany`

(cherry picked from commit 858ba188768017764c61c4a5591bdf2524a850e7)

* Update Changelog

* origin

* Update Changelog

---------

Co-authored-by: Hazel K <acomputerdog@gmail.com>
2024-08-11 11:28:07 +09:00
かっこかり 046f2435b2
fix(frontend): mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 (#14390)
* fix(frontend): mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正

* Update Changelog

* refactor
2024-08-11 11:17:56 +09:00
syuilo 37c9d91ba0 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-08-10 21:07:37 +09:00
syuilo 93c569c2cd refactor 2024-08-10 21:07:33 +09:00
かっこかり cb10156f01
fix(frontend): iOSでacctなどがリンクとして誤検知されるのを抑制 (#14354)
* fix(frontend): iosでの自動リンク化を抑制

* Update Changelog

* typo

* typo

* Update CHANGELOG.md
2024-08-10 20:46:26 +09:00
anatawa12 1532d5f390
ci: skip chromatic / storybook CI for pull requests targets master (#14377) 2024-08-10 09:36:10 +09:00
かっこかり 7e3dedb045
fix(frontend): ページ遷移に失敗することがある問題を修正 (bump vue version) (#14380)
* fix(frontend): ページ遷移に失敗することがある問題を修正 (bump vue version)

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-08-10 09:35:50 +09:00
zyoshoka 01a815f8a7
fix(general): some fixes and improvements of Play visibility (#14384)
* fix(backend): missing `visibility` param in packing flash

* fix(frontend): use `visibility` value got from API

* enhance(frontend): change preview appearance of private Play

* Update CHANGELOG.md
2024-08-10 09:34:49 +09:00
anatawa12 f50941389d
fix: readAllNotifications message not working (#14374)
* refactor: add and use isJsonObject

* fix: readNotification message without body is not working

* docs(changelog): WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題

* Update CHANGELOG.md

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>

---------

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2024-08-09 16:04:41 +09:00
Daiki Mizukami 0d508db8a7
fix(backend): check visibility of following/followers of remote users / feat: moderators can see following/followers of all users (#14375)
* fix(backend): check visibility of following/followers of remote users

Resolves https://github.com/misskey-dev/misskey/issues/13362.

* test(backend): add tests for visibility of following/followers of remote users

* docs(changelog): update CHANGELOG.md

* feat: moderators can see following/followers of all users

* docs(changelog): update CHANGELOG.md

* refactor(backend): minor refactoring

`createPerson`と`if`の条件を統一するとともに、異常系の
処理をearly returnに追い出すための変更。

* feat(backend): moderators can see following/followers count of all users

As per https://github.com/misskey-dev/misskey/pull/14375#issuecomment-2275044908.
2024-08-09 12:10:51 +09:00
anatawa12 f244d42500
ci: change prerelease channels to alpha, beta, and rc (#14376) 2024-08-09 12:05:28 +09:00
syuilo 820becb4e4
fix import 2024-08-09 10:51:18 +09:00
syuilo 6e3e7d7df1 Update about-misskey.vue 2024-08-08 20:22:25 +09:00
github-actions[bot] 008a66d73f [skip ci] Update CHANGELOG.md (prepend template) 2024-07-31 11:20:33 +00:00
github-actions[bot] 59e2e43a68 Release: 2024.7.0 2024-07-31 11:20:28 +00:00
syuilo 1a521a44c0
New Crowdin updates (#14335)
* New translations ja-jp.yml (Japanese, Kansai)

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

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Spanish)

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

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Chinese Traditional)
2024-07-31 18:13:20 +09:00
taichan d6ba12e24c
Fix(frontend): LTL無効時にヘッダーにSTLが表示されてしまう & デフォルト、クラシックでリストが消えている (#14337)
* Fix condition of STL available

* Fix: condition of stl

* Listがタイムラインのヘッダーから消えている
2024-07-31 18:10:36 +09:00
taichan 4b04b2989b
chore(locale): update index.d.ts (#14339)
d63b854f96 での更新漏れ
2024-07-31 17:22:51 +09:00
441 changed files with 13162 additions and 2159 deletions

View File

@ -0,0 +1,203 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
url: 'http://misskey.local'
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey requires a reverse proxy to support HTTPS connections.
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to set up a reverse proxy. (e.g. nginx)
# An encrypted connection with HTTPS is highly recommended
# because tokens may be transferred in GET requests.
# The port that your Misskey server should listen on.
port: 61812
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: db
port: 5432
# Database name
db: misskey
# Auth
user: postgres
pass: postgres
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#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: redis
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#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
#redisForTimelines:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
#meilisearch:
# host: meilisearch
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: 'aidx'
# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────
# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.
#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 32
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
proxyBypassHosts:
- api.deepl.com
- api-free.deepl.com
- www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: true)
proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@ -3,6 +3,8 @@
set -xe
sudo chown node node_modules
sudo apt-get update
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
git config --global --add safe.directory /workspace
git submodule update --init
corepack install
@ -12,3 +14,4 @@ pnpm install --frozen-lockfile
cp .devcontainer/devcontainer.yml .config/default.yml
pnpm build
pnpm migrate
pnpm exec cypress install

View File

@ -48,12 +48,14 @@ jobs:
"packages/backend/migration"
"packages/backend/src"
"packages/backend/test"
"packages/frontend-shared/src"
"packages/frontend/.storybook"
"packages/frontend/@types"
"packages/frontend/lib"
"packages/frontend/public"
"packages/frontend/src"
"packages/frontend/test"
"packages/frontend-embed/src"
"packages/misskey-bubble-game/src"
"packages/misskey-reversi/src"
"packages/sw/src"

View File

@ -8,6 +8,8 @@ on:
paths:
- packages/backend/**
- packages/frontend/**
- packages/frontend-shared/**
- packages/frontend-embed/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/eslint.config.js
@ -16,6 +18,8 @@ on:
paths:
- packages/backend/**
- packages/frontend/**
- packages/frontend-shared/**
- packages/frontend-embed/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/eslint.config.js
@ -40,15 +44,18 @@ jobs:
needs: [pnpm_install]
runs-on: ubuntu-latest
continue-on-error: true
env:
eslint-cache-version: v1
strategy:
matrix:
workspace:
- backend
- frontend
- frontend-shared
- frontend-embed
- sw
- misskey-js
env:
eslint-cache-version: v1
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
steps:
- uses: actions/checkout@v4.1.1
with:
@ -64,11 +71,10 @@ jobs:
- name: Restore eslint cache
uses: actions/cache@v4.0.2
with:
path: node_modules/.cache/eslint
key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
- run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location ${{ env.eslint-cache-path }} --cache-strategy content
typecheck:
needs: [pnpm_install]
@ -78,6 +84,7 @@ jobs:
matrix:
workspace:
- backend
- sw
- misskey-js
steps:
- uses: actions/checkout@v4.1.1
@ -92,7 +99,7 @@ jobs:
- run: corepack enable
- run: pnpm i --frozen-lockfile
- run: pnpm --filter misskey-js run build
if: ${{ matrix.workspace == 'backend' }}
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
- run: pnpm --filter misskey-reversi run build
if: ${{ matrix.workspace == 'backend' }}
- run: pnpm --filter ${{ matrix.workspace }} run typecheck

View File

@ -6,7 +6,7 @@ on:
- develop
paths:
- 'CHANGELOG.md'
# - .github/workflows/release-edit-with-push.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -17,6 +17,10 @@ on:
type: boolean
description: 'MERGE RELEASE BRANCH TO MAIN'
default: false
start-rc:
type: boolean
description: 'Start Release Candidate'
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -79,6 +83,9 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
draft_prerelease_channel: alpha
ready_start_prerelease_channel: beta
prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }}
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
@ -122,6 +129,7 @@ jobs:
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
stable_branch: ${{ vars.STABLE_BRANCH }}
draft_prerelease_channel: alpha
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

View File

@ -39,6 +39,8 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }}
draft_prerelease_channel: alpha
ready_start_prerelease_channel: beta
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

View File

@ -7,6 +7,11 @@ on:
- develop
- dev/storybook8 # for testing
pull_request_target:
branches-ignore:
# Since pull requests targets master mostly is the "develop" branch.
# Storybook CI is checked on the "push" event of "develop" branch so it would cause a duplicate build.
# This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master.
- master
jobs:
build:

4
.gitignore vendored
View File

@ -35,6 +35,9 @@ coverage
!/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env
!/.config/cypress-devcontainer.yml
docker-compose.yml
compose.yml
.devcontainer/compose.yml
!/.devcontainer/compose.yml
@ -42,6 +45,7 @@ coverage
/build
built
built-test
js-built
/data
/.cache-loader
/db

View File

@ -1,3 +1,62 @@
## Unreleased
### General
-
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
- Enhance: アイコンデコレーション管理画面にプレビューを追加
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
### Server
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
## 2024.8.0
### General
- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
- Enhance: アカウントの削除のモデレーションログを残すように
- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
### Client
- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
- Fix: ページ遷移に失敗することがある問題を修正
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正
- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
### Server
- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
- 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
- これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。
- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正
- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582)
- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679)
- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように
- キュー処理のつまりが改善される可能性があります
- Fix: リバーシの対局設定の変更が反映されないのを修正
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
- Fix: Prevent memory leak from memory caches (#14310)
- Fix: More reliable memory cache eviction (#14311)
## 2024.7.0
### Note

View File

@ -21,7 +21,9 @@ WORKDIR /misskey
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]

View File

@ -2010,7 +2010,6 @@ _webhookSettings:
createWebhook: "Vytvořit Webhook"
name: "Jméno"
secret: "Tajné"
events: "Události Webhook"
active: "Zapnuto"
_events:
follow: "Při sledování uživatele"

View File

@ -2191,7 +2191,6 @@ _webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"

View File

@ -182,7 +182,7 @@ addAccount: "Add account"
reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance"
continueOnRemote: "リモートで続行"
continueOnRemote: "Continue on a remote server"
chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub"
specifyServerHost: "Specify a server host directly"
inputHostName: "Enter the domain"
@ -398,7 +398,7 @@ mcaptcha: "mCaptcha"
enableMcaptcha: "Enable mCaptcha"
mcaptchaSiteKey: "Site key"
mcaptchaSecretKey: "Secret key"
mcaptchaInstanceUrl: "mCaptcha instance URL"
mcaptchaInstanceUrl: "mCaptcha server URL"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Enable reCAPTCHA"
recaptchaSiteKey: "Site key"
@ -487,7 +487,7 @@ noMessagesYet: "No messages yet"
newMessageExists: "There are new messages"
onlyOneFileCanBeAttached: "You can only attach one file to a message"
signinRequired: "Please register or sign in before continuing"
signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
invitations: "Invites"
invitationCode: "Invitation code"
checking: "Checking..."
@ -1255,7 +1255,7 @@ launchApp: "Launch the app"
useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio"
keepOriginalFilename: "Keep original file name"
keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files."
noDescription: "There is not the explanation"
noDescription: "There is no explanation"
alwaysConfirmFollow: "Always confirm when following"
inquiry: "Contact"
tryAgain: "Please try again later"
@ -1365,7 +1365,7 @@ _initialTutorial:
_exampleNote:
cw: "This will surely make you hungry!"
note: "Just had a chocolate-glazed donut 🍩😋"
useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text."
useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text."
_howToMakeAttachmentsSensitive:
title: "How to Mark Attachments as Sensitive?"
description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
@ -2316,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "Set thumbnail"
eyeCatchingImageRemove: "Delete thumbnail"
chooseBlock: "Add a block"
enterSectionTitle: "Enter a section title"
selectType: "Select a type"
contentBlocks: "Content"
inputBlocks: "Input"
@ -2426,7 +2427,7 @@ _webhookSettings:
modifyWebhook: "Modify Webhook"
name: "Name"
secret: "Secret"
events: "Webhook Events"
trigger: "Trigger"
active: "Enabled"
_events:
follow: "When following a user"
@ -2494,11 +2495,15 @@ _moderationLogTypes:
unsetUserAvatar: "Unset this user's avatar"
unsetUserBanner: "Unset this user's banner"
createSystemWebhook: "Create SystemWebhook"
updateSystemWebhook: "Update SystemWebHook"
updateSystemWebhook: "Update SystemWebhook"
deleteSystemWebhook: "Delete SystemWebhook"
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
deleteAccount: "Delete the account"
deletePage: "Delete the page"
deleteFlash: "Delete Play"
deleteGalleryPost: "Delete the gallery post"
_fileViewer:
title: "File details"
type: "File type"

View File

@ -60,6 +60,7 @@ copyFileId: "Copiar ID del archivo"
copyFolderId: "Copiar ID de carpeta"
copyProfileUrl: "Copiar la URL del perfil"
searchUser: "Buscar un usuario"
searchThisUsersNotes: ""
reply: "Responder"
loadMore: "Ver más"
showMore: "Ver más"
@ -2382,7 +2383,6 @@ _webhookSettings:
createWebhook: "Crear Webhook"
name: "Nombre"
secret: "Secreto"
events: "Eventos de webhook"
active: "Activado"
_events:
follow: "Cuando se sigue a alguien"

View File

@ -1094,6 +1094,8 @@ preservedUsernames: "Noms d'utilisateur·rice réservés"
preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés."
createNoteFromTheFile: "Rédiger une note de ce fichier"
archive: "Archive"
archived: "Archivé"
unarchive: "Annuler l'archivage"
channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?"
channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible."
thisChannelArchived: "Ce canal a été archivé."
@ -1224,7 +1226,10 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet"
loading: "Chargement en cours"
surrender: "Annuler"
gameRetry: "Réessayer"
launchApp: "Lancer l'app"
inquiry: "Contact"
_delivery:
status: "Statut de la diffusion"
stop: "Suspendu·e"
_type:
none: "Publié"

View File

@ -2403,7 +2403,6 @@ _webhookSettings:
modifyWebhook: "Sunting Webhook"
name: "Nama"
secret: "Secret"
events: "Webhook Events"
active: "Aktif"
_events:
follow: "Ketika mengikuti pengguna"

96
locales/index.d.ts vendored
View File

@ -2829,7 +2829,7 @@ export interface Locale extends ILocale {
*/
"reportAbuseOf": ParameterizedString<"name">;
/**
* URLも記入してください
* URLも記入してください
*/
"fillAbuseReportDescription": string;
/**
@ -5068,6 +5068,22 @@ export interface Locale extends ILocale {
*
*/
"createdAntennas": string;
/**
* {x}
*/
"fromX": ParameterizedString<"x">;
/**
*
*/
"genEmbedCode": string;
/**
*
*/
"noteOfThisUser": string;
/**
*
*/
"clipNoteLimitExceeded": string;
/**
*
*/
@ -8997,6 +9013,10 @@ export interface Locale extends ILocale {
*
*/
"chooseBlock": string;
/**
*
*/
"enterSectionTitle": string;
/**
*
*/
@ -9414,9 +9434,9 @@ export interface Locale extends ILocale {
*/
"secret": string;
/**
* Webhookを実行するタイミング
*
*/
"events": string;
"trigger": string;
/**
*
*/
@ -9691,6 +9711,22 @@ export interface Locale extends ILocale {
*
*/
"deleteAbuseReportNotificationRecipient": string;
/**
*
*/
"deleteAccount": string;
/**
*
*/
"deletePage": string;
/**
* Playを削除
*/
"deleteFlash": string;
/**
* 稿
*/
"deleteGalleryPost": string;
};
"_fileViewer": {
/**
@ -10184,6 +10220,60 @@ export interface Locale extends ILocale {
*/
"native": string;
};
"_embedCodeGen": {
/**
*
*/
"title": string;
/**
*
*/
"header": string;
/**
*
*/
"autoload": string;
/**
*
*/
"maxHeight": string;
/**
* 0
*/
"maxHeightDescription": string;
/**
* 0
*/
"maxHeightWarn": string;
/**
*
*/
"previewIsNotActual": string;
/**
*
*/
"rounded": string;
/**
*
*/
"border": string;
/**
*
*/
"applyToPreview": string;
/**
*
*/
"generateCode": string;
/**
*
*/
"codeGenerated": string;
/**
*
*/
"codeGeneratedDescription": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -2412,7 +2412,6 @@ _webhookSettings:
modifyWebhook: "Modifica Webhook"
name: "Nome"
secret: "Segreto"
events: "Quando eseguire il Webhook"
active: "Attivo"
_events:
follow: "Quando segui un profilo"

View File

@ -703,7 +703,7 @@ abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノートを通報"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。"
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
reporter: "通報者"
reporteeOrigin: "通報先"
@ -1263,6 +1263,10 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
fromX: "{x}から"
genEmbedCode: "埋め込みコードを生成"
noteOfThisUser: "このユーザーのノート一覧"
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
pageViewCount: "ページ閲覧数"
preferPopularUserFactor: "人気のユーザーの算出基準"
preferPopularUserFactorDescription: "ページ閲覧数はローカルユーザーにのみ適用されます(リモートユーザーはフォロワー数で表示されます)。「無効」に設定すると、ローカル・リモートどちらの「人気のユーザー」セクションも表示されなくなります。"
@ -2374,6 +2378,7 @@ _pages:
eyeCatchingImageSet: "アイキャッチ画像を設定"
eyeCatchingImageRemove: "アイキャッチ画像を削除"
chooseBlock: "ブロックを追加"
enterSectionTitle: "セクションタイトルを入力"
selectType: "種類を選択"
contentBlocks: "コンテンツ"
inputBlocks: "入力"
@ -2570,6 +2575,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "通報の通知先を作成"
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
deleteAccount: "アカウントを削除"
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
_fileViewer:
title: "ファイルの詳細"
@ -2715,3 +2724,18 @@ _contextMenu:
app: "アプリケーション"
appWithShift: "Shiftキーでアプリケーション"
native: "ブラウザのUI"
_embedCodeGen:
title: "埋め込みコードをカスタマイズ"
header: "ヘッダーを表示"
autoload: "自動で続きを読み込む(非推奨)"
maxHeight: "高さの最大値"
maxHeightDescription: "0で最大値の設定が無効になります。ウィジェットが縦に伸び続けるのを防ぐために、何らかの値に指定してください。"
maxHeightWarn: "高さの最大値制限が無効0になっています。これが意図した変更ではない場合は、高さの最大値を何らかの値に設定してください。"
previewIsNotActual: "プレビュー画面で表示可能な範囲を超えたため、実際に埋め込んだ際とは表示が異なります。"
rounded: "角丸にする"
border: "外枠に枠線をつける"
applyToPreview: "プレビューに反映"
generateCode: "埋め込みコードを作成"
codeGenerated: "コードが生成されました"
codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"

View File

@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを探す"
searchThisUsersNotes: "ユーザーのノートを検索"
reply: "返事"
loadMore: "まだまだあるで!"
showMore: "まだまだあるで!"
@ -114,6 +115,8 @@ cantReRenote: "リノート自体はリノートできへんで。"
quote: "引用"
inChannelRenote: "チャンネルの中でリノート"
inChannelQuote: "チャンネル内引用"
renoteToChannel: "チャンネルにリノート"
renoteToOtherChannel: "他のチャンネルにリノート"
pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく"
you: "あんた"
@ -152,6 +155,7 @@ editList: "リストいじる"
selectChannel: "チャンネルを選ぶ"
selectAntenna: "アンテナを選ぶ"
editAntenna: "アンテナいじる"
createAntenna: "アンテナを作成"
selectWidget: "ウィジェットを選ぶ"
editWidgets: "ウィジェットをいじる"
editWidgetsExit: "いじるのをやめる"
@ -178,6 +182,10 @@ addAccount: "アカウントを追加"
reloadAccountsList: "アカウントリストの情報を更新"
loginFailed: "ログインに失敗してもうた…"
showOnRemote: "リモートで見る"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
specifyServerHost: "サーバーのドメインを直接指定"
inputHostName: "ドメインを入力せえや"
general: "全般"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
@ -188,6 +196,7 @@ followConfirm: "{name}をフォローしてええか?"
proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
host: "ホスト"
selectSelf: "自分を選択"
selectUser: "ユーザーを選ぶ"
recipient: "宛先"
annotation: "注釈"
@ -203,6 +212,7 @@ perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送をやめる"
blockThisInstance: "このサーバーをブロックすんで"
silenceThisInstance: "サーバーサイレンスすんで?"
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
@ -224,6 +234,8 @@ blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。"
silencedInstances: "サーバーサイレンスされてんねん"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
mediaSilencedInstances: "メディアサイレンスしたサーバー"
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定するで。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われてな、カスタム絵文字が使えへんようになるで。ブロックしたインスタンスには影響せえへんで。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしとるユーザー"
blockedUsers: "ブロックしとるユーザー"
@ -475,6 +487,7 @@ noMessagesYet: "まだチャットはあらへんで"
newMessageExists: "新しいメッセージがきたで"
onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
signinRequired: "ログインしてくれへん?"
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があるで"
invitations: "来てや"
invitationCode: "招待コード"
checking: "確認しとるで"
@ -1025,6 +1038,7 @@ thisPostMayBeAnnoyingHome: "ホームに投稿"
thisPostMayBeAnnoyingCancel: "やめとく"
thisPostMayBeAnnoyingIgnore: "このまま投稿"
collapseRenotes: "見たことあるリノートは飛ばして表示するで"
collapseRenotesDescription: "リアクションやリノートをしたことがあるノートをたたんで表示するで。"
internalServerError: "サーバー内部エラー"
internalServerErrorDescription: "サーバーでなんか変なこと起こっとるわ。"
copyErrorInfo: "エラー情報をコピるで"
@ -1098,6 +1112,8 @@ preservedUsernames: "予約ユーザー名"
preservedUsernamesDescription: "予約しとくユーザー名を行ごとに挙げるで。ここで指定されたユーザー名はアカウント作るときに使えへんくなるけど、管理者は例外や。あと、もうあるアカウントも例外やな。"
createNoteFromTheFile: "このファイル使うてノート作るで"
archive: "アーカイブ"
archived: "アーカイブ済み"
unarchive: "アーカイブ解除"
channelArchiveConfirmTitle: "{name}をアーカイブしてええか?"
channelArchiveConfirmDescription: "アーカイブしたら、チャンネル一覧とか検索結果からなくなるし、新しく書き込みもできへんなるで。"
thisChannelArchived: "このチャンネル、アーカイブされとるで。"
@ -1108,6 +1124,9 @@ preventAiLearning: "生成AIの学習に使わんといて"
preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。"
options: "オプション"
specifyUser: "ユーザー指定"
lookupConfirm: "照会するけどええか?"
openTagPageConfirm: "ハッシュタグのページを開くんか?"
specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできへん"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール"
@ -1239,10 +1258,20 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はあらへんで"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "問い合わせ"
tryAgain: "もう一度試しいや。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアやで。表示するんか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
_delivery:
status: "配信状態"
stop: "配信せぇへん"
resume: "配信再開"
_type:
none: "配信しとる"
manuallySuspended: "手動停止中"
goneSuspended: "サーバー削除のため停止中"
autoSuspendedForNotResponding: "サーバー応答せえへんから停止中"
_bubbleGame:
howToPlay: "遊び方"
hold: "ホールド"
@ -1368,6 +1397,8 @@ _serverSettings:
fanoutTimelineDescription: "入れると、おのおのタイムラインを取得するときにめちゃめちゃ動きが良うなって、データベースが軽くなるわ。でも、Redisのメモリ使う量が増えるから注意な。サーバーのメモリが足りんときとか、動きが変なときは切れるで。"
fanoutTimelineDbFallback: "データベースにフォールバックする"
fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromSub: "別のアカウントへエイリアスを作る"
@ -1684,6 +1715,7 @@ _role:
canManageAvatarDecorations: "アバターを飾るモンの管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける"
canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートピン留めできる数"
antennaMax: "アンテナ作れる数"
wordMuteMax: "ワードミュートの最大文字数"
@ -1935,6 +1967,7 @@ _soundSettings:
driveFileTypeWarnDescription: "音声ファイルを選びや"
driveFileDurationWarn: "音が長すぎるわ"
driveFileDurationWarnDescription: "長い音使うたらMisskey使うのに良うないかもしれへんで。それでもええか"
driveFileError: "音声が読み込めへんかったで。設定を変更せえや"
_ago:
future: "未来"
justNow: "ついさっき"
@ -2351,6 +2384,7 @@ _deck:
alwaysShowMainColumn: "いつもメインカラムを表示"
columnAlign: "カラムの寄せ"
addColumn: "カラムを追加"
newNoteNotificationSettings: "新着ノート通知の設定"
configureColumn: "カラムの設定"
swapLeft: "左に移動"
swapRight: "右に移動"
@ -2389,9 +2423,10 @@ _drivecleaner:
orderByCreatedAtAsc: "追加日の古い順"
_webhookSettings:
createWebhook: "Webhookをつくる"
modifyWebhook: "Webhookを編集"
name: "名前"
secret: "シークレット"
events: "Webhookを投げるタイミング"
trigger: "トリガー"
active: "有効"
_events:
follow: "フォローしたとき~!"
@ -2401,11 +2436,25 @@ _webhookSettings:
renote: "リノートされるとき~!"
reaction: "ツッコまれたとき~!"
mention: "メンションがあるとき~!"
_systemEvents:
abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき"
deleteConfirm: "ほんまにWebhookをほかしてもええんか"
_abuseReport:
_notificationRecipient:
createRecipient: "通報の通知先を追加"
modifyRecipient: "通報の通知先を編集"
recipientType: "通知先の種類"
_recipientType:
mail: "メール"
webhook: "Webhook"
_captions:
mail: "モデレーター権限を持つユーザーのメアドに通知を送るで(通報を受けた時のみ)"
webhook: "指定したSystemWebhookに通知を送るで(通報を受けた時と通報を解決した時にそれぞれ発信)"
keywords: "キーワード"
notifiedUser: "通知先ユーザー"
notifiedWebhook: "使用するWebhook"
deleteConfirm: "通知先を削除してもええか?"
_moderationLogTypes:
createRole: "ロールを追加すんで"
@ -2444,6 +2493,8 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "この子のアイコン元に戻す"
unsetUserBanner: "この子のバナー元に戻す"
createSystemWebhook: "SystemWebhookを作成"
updateSystemWebhook: "SystemWebhookを更新"
_fileViewer:
title: "ファイルの詳しい情報"
type: "ファイルの種類"

View File

@ -2411,7 +2411,6 @@ _webhookSettings:
modifyWebhook: "Webhook 수정"
name: "이름"
secret: "시크릿"
events: "Webhook을 실행할 타이밍"
active: "활성화"
_events:
follow: "누군가를 팔로우했을 때"

View File

@ -1544,7 +1544,6 @@ _webhookSettings:
createWebhook: "Stwórz Webhook"
name: "Nazwa"
secret: "Sekret"
events: "Uruchomienie Webhooka"
active: "Właczono"
_events:
follow: "Po zaobserwowaniu użytkownika"

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,7 @@ copyFileId: "คัดลอกไฟล์ ID"
copyFolderId: "คัดลอกโฟลเดอร์ ID"
copyProfileUrl: "คัดลอกโปรไฟล์ URL"
searchUser: "ค้นหาผู้ใช้"
searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้"
reply: "ตอบกลับ"
loadMore: "แสดงเพิ่มเติม"
showMore: "แสดงเพิ่มเติม"
@ -154,6 +155,7 @@ editList: "แก้ไขรายชื่อ"
selectChannel: "เลือกช่อง"
selectAntenna: "เลือกเสาอากาศ"
editAntenna: "แก้ไขเสาอากาศ"
createAntenna: "สร้างเสาอากาศ"
selectWidget: "เลือกวิดเจ็ต"
editWidgets: "แก้ไขวิดเจ็ต"
editWidgetsExit: "เรียบร้อย"
@ -194,6 +196,7 @@ followConfirm: "ต้องการติดตาม {name} ใช่ไห
proxyAccount: "บัญชีพร็อกซี่"
proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น"
host: "โฮสต์"
selectSelf: "เลือกตัวเอง"
selectUser: "เลือกผู้ใช้งาน"
recipient: "ผู้รับ"
annotation: "หมายเหตุประกอบ"
@ -209,6 +212,7 @@ perDay: "ต่อวัน"
stopActivityDelivery: "หยุดส่งกิจกรรม"
blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้"
silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้"
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
operations: "ดำเนินการ"
software: "ซอฟต์แวร์"
version: "เวอร์ชั่น"
@ -230,6 +234,8 @@ blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็
blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้"
silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว"
silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ"
mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก"
muteAndBlock: "ปิดเสียงและบล็อก"
mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง"
blockedUsers: "ผู้ใช้ที่ถูกบล็อก"
@ -881,7 +887,7 @@ accountDeletionInProgress: "กำลังดำเนินการลบบ
usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
aiChanMode: "โหมด Ai "
devMode: "โหมดนักพัฒนา"
keepCw: "เก็บคำเตือนเนื้อหา"
keepCw: "คงการเตือนเนื้อหาไว้"
pubSub: "บัญชี Pub/Sub"
lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
resolved: "คลี่คลายแล้ว"
@ -1028,15 +1034,15 @@ achievements: "ความสำเร็จ"
gotInvalidResponseError: "การตอบสนองเซิร์ฟเวอร์ไม่ถูกต้อง"
gotInvalidResponseErrorDescription: "เซิร์ฟเวอร์อาจไม่สามารถเข้าถึงได้หรืออาจจะกำลังอยู่ในระหว่างปรับปรุง กรุณาลองใหม่อีกครั้งในภายหลังนะคะ"
thisPostMayBeAnnoying: "โน้ตนี้อาจจะเป็นการรบกวนผู้อื่นนะคะ"
thisPostMayBeAnnoyingHome: "โพสต์ไปยังไทม์ไลน์หลัก"
thisPostMayBeAnnoyingCancel: "เลิก"
thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้วแต่"
thisPostMayBeAnnoyingHome: "โพสต์ลงไทม์ไลน์หลักเท่านั้น"
thisPostMayBeAnnoyingCancel: "ยกเลิก"
thisPostMayBeAnnoyingIgnore: "โพสต์ไปเลย ไม่ต้องปรับการมองเห็น"
collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว"
collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว"
internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด"
internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์"
copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด"
joinThisServer: "ลงทะเบียนนเซิร์ฟเวอร์นี้"
joinThisServer: "ลงทะเบียนนเซิร์ฟเวอร์นี้"
exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น"
letsLookAtTimeline: "มาดูไทม์ไลน์กัน"
disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?"
@ -1099,13 +1105,15 @@ vertical: "แนวตั้ง"
horizontal: "แนวนอน"
position: "ตำแหน่ง"
serverRules: "กฎของเซิร์ฟเวอร์"
pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนนเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
pleaseConfirmBelowBeforeSignup: "หากต้องการลงทะเบียนนเซิร์ฟเวอร์นี้ คุณต้องตรวจสอบและยอมรับสิ่งต่อไปนี้"
pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
continue: "ดำเนินการต่อ"
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ"
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
archive: "เก็บถาวร"
archived: "เก็บถาวรแล้ว"
unarchive: "เลิกการเก็บถาวร"
channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?"
channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป"
thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
@ -1116,6 +1124,9 @@ preventAiLearning: "ปฏิเสธการเรียนรู้ด้ว
preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
options: "ตัวเลือกบทบาท"
specifyUser: "ผู้ใช้เฉพาะ"
lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
specifyHost: "ระบุโฮสต์"
failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
update: "อัปเดต"
rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้"
@ -1250,6 +1261,8 @@ inquiry: "ติดต่อเรา"
tryAgain: "โปรดลองอีกครั้ง"
confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน"
sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?"
createdLists: "รายชื่อที่ถูกสร้าง"
createdAntennas: "เสาอากาศที่ถูกสร้าง"
_delivery:
status: "สถานะการจัดส่ง"
stop: "ระงับการส่ง"
@ -1348,9 +1361,9 @@ _initialTutorial:
localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
_cw:
title: "คำเตือนเกี่ยวกับเนื้อหา"
description: "เนื้อหาที่เขียนด้วย “คำอธิบายประกอบ” จะแสดงแทนข้อความหลัก คลิก “ดูเพิ่มเติม” เพื่อแสดงข้อความเต็ม"
description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
_exampleNote:
cw: "นี่อาจจะทำให้คุณหิวอย่างแน่นอน!"
cw: " ห้ามดู ระวังหิว"
note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
useCases: "ใช้สิ่งนี้เพื่อระบุโน้ตที่ต้องตามแนวทางปฏิบัติของเซิร์ฟเวอร์ หรือเพื่อควบคุมการสปอยล์และข้อความที่ละเอียดอ่อนด้วยตนเอง"
_howToMakeAttachmentsSensitive:
@ -1466,15 +1479,15 @@ _achievements:
title: "มือใหม่ III"
description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
_login30:
title: "มิสคิส์ I"
title: "มิสคิส์ I"
description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
_login60:
title: "มิสคิส์ II"
title: "มิสคิส์ II"
description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
_login100:
title: "มิสคิส์ III"
title: "มิสคิส์ III"
description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
flavor: "มิสคิสต์หัวรุนแรง"
flavor: "Violent Misskist (ทำไมเหมือนชื่อหนังสักเรื่องจังเลยนะ)"
_login200:
title: "ลูกค้าประจำ I"
description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
@ -1954,6 +1967,7 @@ _soundSettings:
driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง"
driveFileDurationWarn: "เสียงยาวเกินไป"
driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?"
driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า"
_ago:
future: "อนาคต"
justNow: "เมื่อกี๊นี้"
@ -2141,7 +2155,7 @@ _widgets:
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
aiscript: " คอนโซล AiScript"
aiscriptApp: "แอป AiScript"
aichan: "ไอ"
aichan: "藍 (ไอ)"
userList: "รายชื่อผู้ใช้"
_userList:
chooseList: "เลือกรายชื่อ"
@ -2183,7 +2197,7 @@ _visibility:
followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มองเห็นได้"
specified: "ไดเร็ค"
specifiedDescription: "ทำให้มองเห็นได้เฉพาะผู้ใช้ที่ระบุเท่านั้น"
disableFederation: "ไม่มีสหพันธ์"
disableFederation: "การปิดใช้งานสหพันธ์"
disableFederationDescription: "อย่าส่งข้อมูลไปยังเซิร์ฟเวอร์อื่น"
_postForm:
replyPlaceholder: "ตอบกลับโน้ตนี้..."
@ -2412,7 +2426,7 @@ _webhookSettings:
modifyWebhook: "แก้ไข Webhook"
name: "ชื่อ"
secret: "ความลับ"
events: "อีเว้นท์ Webhook"
trigger: "ทริกเกอร์"
active: "เปิดใช้งาน"
_events:
follow: "เมื่อกำลังติดตามผู้ใช้"
@ -2536,7 +2550,7 @@ _externalResourceInstaller:
description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
_dataSaver:
_media:
title: "โหลดมีเดีย"
title: "โหลดสื่อ"
description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
_avatar:
title: "รูปไอคอน"
@ -2616,3 +2630,8 @@ _mediaControls:
pip: "รูปภาพในรูปภาม"
playbackRate: "ความเร็วในการเล่น"
loop: "เล่นวนซ้ำ"
_contextMenu:
title: "เมนูเนื้อหา"
app: "แอปพลิเคชัน"
appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)"
native: "UI ของเบราว์เซอร์"

View File

@ -1918,7 +1918,6 @@ _webhookSettings:
createWebhook: "Tạo Webhook"
name: "Tên"
secret: "Mã bí mật"
events: "Sự kiện Webhook"
active: "Đã bật"
_events:
reaction: "Khi nhận được sự kiện"

View File

@ -1665,6 +1665,7 @@ _achievements:
_bubbleGameDoubleExplodingHead:
title: "两个🤯"
description: "你合成出了2个游戏里最大的Emoji"
flavor: ""
_role:
new: "创建角色"
edit: "编辑角色"
@ -2315,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "设置封面图片"
eyeCatchingImageRemove: "删除封面图片"
chooseBlock: "添加块"
enterSectionTitle: "输入会话标题"
selectType: "选择类型"
contentBlocks: "内容"
inputBlocks: "输入"
@ -2425,7 +2427,7 @@ _webhookSettings:
modifyWebhook: "编辑 webhook"
name: "名称"
secret: "密钥"
events: "何时运行 Webhook"
trigger: "触发器"
active: "已启用"
_events:
follow: "关注时"
@ -2498,6 +2500,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "新建了举报通知"
updateAbuseReportNotificationRecipient: "更新了举报通知"
deleteAbuseReportNotificationRecipient: "删除了举报通知"
deleteAccount: "删除了账户"
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件"
_fileViewer:
title: "文件信息"
type: "文件类型"

View File

@ -1967,7 +1967,7 @@ _soundSettings:
driveFileTypeWarnDescription: "請選擇音效檔案"
driveFileDurationWarn: "音效太長了"
driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?"
driveFileError: "無法載入語音。請設定"
driveFileError: "無法載入語音。請更設定"
_ago:
future: "未來"
justNow: "剛剛"
@ -2316,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "設定封面影像"
eyeCatchingImageRemove: "刪除封面影像"
chooseBlock: "新增方塊"
enterSectionTitle: "輸入區段的標題"
selectType: "選擇類型"
contentBlocks: "內容"
inputBlocks: "輸入"
@ -2426,7 +2427,7 @@ _webhookSettings:
modifyWebhook: "編輯 Webhook"
name: "名字"
secret: "密鑰"
events: "何時運行 Webhook"
trigger: "觸發器"
active: "已啟用"
_events:
follow: "當你追隨時"
@ -2439,6 +2440,7 @@ _webhookSettings:
_systemEvents:
abuseReport: "當使用者檢舉時"
abuseReportResolved: "當處理了使用者的檢舉時"
userCreated: "使用者被新增時"
deleteConfirm: "請問是否要刪除 Webhook"
_abuseReport:
_notificationRecipient:
@ -2498,6 +2500,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
deleteAccount: "刪除帳戶"
deletePage: "刪除頁面"
deleteFlash: "刪除 Play"
deleteGalleryPost: "刪除相簿的貼文"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
@ -2632,4 +2638,5 @@ _mediaControls:
_contextMenu:
title: "內容功能表"
app: "應用程式"
appWithShift: "Shift 鍵應用程式"
native: "瀏覽器的使用者介面"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.7.0-rc.8",
"version": "2024.8.0",
"codename": "nasubi",
"repository": {
"type": "git",
@ -8,7 +8,9 @@
},
"packageManager": "pnpm@9.6.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
"packages/frontend-embed",
"packages/backend",
"packages/sw",
"packages/misskey-js",
@ -35,6 +37,7 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm -r test",
@ -61,7 +64,7 @@
"glob": "11.0.0"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.2",
"@misskey-dev/eslint-plugin": "2.0.3",
"@types/node": "20.14.12",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",

View File

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: MIT
*/
//@ts-check
(() => {
/** @type {NodeListOf<HTMLIFrameElement>} */
const els = document.querySelectorAll('iframe[data-misskey-embed-id]');
window.addEventListener('message', function (event) {
els.forEach((el) => {
if (event.source !== el.contentWindow) {
return;
}
const id = el.dataset.misskeyEmbedId;
if (event.data.type === 'misskey:embed:ready') {
el.contentWindow?.postMessage({
type: 'misskey:embedParent:registerIframeId',
payload: {
iframeId: id,
}
}, '*');
}
if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) {
el.style.height = event.data.payload.height + 'px';
}
});
});
})();

View File

@ -100,7 +100,7 @@
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"body-parser": "1.20.3",
"bullmq": "5.10.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
@ -119,7 +119,7 @@
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.4.2",
"happy-dom": "10.0.3",
"happy-dom": "15.6.1",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",

View File

@ -133,7 +133,7 @@ export type Config = {
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
maxFileSize: number | undefined;
maxFileSize: number;
clusterLimit: number | undefined;
id: string;
outgoingAddress: string | undefined;
@ -160,8 +160,10 @@ export type Config = {
authUrl: string;
driveUrl: string;
userAgent: string;
clientEntry: string;
clientManifestExists: boolean;
frontendEntry: string;
frontendManifestExists: boolean;
frontendEmbedEntry: string;
frontendEmbedManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null;
@ -196,10 +198,16 @@ const path = process.env.MISSKEY_CONFIG_YML
export function loadConfig(): Config {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
const clientManifest = clientManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
const frontendManifest = frontendManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const frontendEmbedManifest = frontendEmbedManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
: { 'src/boot.ts': { file: 'src/boot.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
@ -250,7 +258,7 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
maxFileSize: config.maxFileSize,
maxFileSize: config.maxFileSize ?? 262144000,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,
outgoingAddressFamily: config.outgoingAddressFamily,
@ -270,8 +278,10 @@ export function loadConfig(): Config {
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
: null,
userAgent: `Misskey/${version} (${config.url})`,
clientEntry: clientManifest['src/_boot_.ts'],
clientManifestExists: clientManifestExists,
frontendEntry: frontendManifest['src/_boot_.ts'],
frontendManifestExists: frontendManifestExists,
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
frontendEmbedManifestExists: frontendEmbedManifestExists,
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),

View File

@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
this.redisForSub.on('message', this.onMessage);
}

View File

@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown {
if (user == null) {
this.userByIdCache.delete(body.id);
this.localUserByIdCache.delete(body.id);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
for (const [k, v] of this.uriPersonCache.entries) {
if (v.value?.id === body.id) {
this.uriPersonCache.delete(k);
}
}
} else {
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
for (const [k, v] of this.uriPersonCache.entries) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}

View File

@ -24,7 +24,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
private cache: MemoryKVCache<MiEmoji | null>;
private emojisCache: MemoryKVCache<MiEmoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
constructor(
@ -40,7 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
@ -334,7 +334,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
host,
})) ?? null;
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
@ -361,7 +361,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
*/
@bindThis
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null);
const emojisQuery: any[] = [];
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
@ -376,7 +376,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}) : [];
for (const emoji of _emojis) {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}
@ -401,7 +401,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
this.cache.dispose();
this.emojisCache.dispose();
}
@bindThis

View File

@ -4,12 +4,15 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository } from '@/models/_.js';
import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class DeleteAccountService {
@ -17,9 +20,14 @@ export class DeleteAccountService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userSuspendService: UserSuspendService,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
}
@ -27,16 +35,52 @@ export class DeleteAccountService {
public async deleteAccount(user: {
id: string;
host: string | null;
}): Promise<void> {
}, moderator?: MiUser): Promise<void> {
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
// 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(e => {});
if (moderator != null) {
this.moderationLogService.log(moderator, 'deleteAccount', {
userId: user.id,
userUsername: _user.username,
userHost: user.host,
});
}
this.queueService.createDeleteAccountJob(user, {
soft: false,
});
// 物理削除する前にDelete activityを送信する
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = [];
const followings = await this.followingsRepository.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
}
for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true);
}
this.queueService.createDeleteAccountJob(user, {
soft: false,
});
} else {
// リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
this.queueService.createDeleteAccountJob(user, {
soft: true,
});
}
await this.usersRepository.update(user.id, {
isDeleted: true,

View File

@ -42,7 +42,7 @@ export class DownloadService {
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const maxSize = this.config.maxFileSize ?? 262144000;
const maxSize = this.config.maxFileSize;
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';

View File

@ -15,7 +15,7 @@ import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size';
import { type predictionType } from 'nsfwjs';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import { encode } from 'blurhash';
import * as blurhash from 'blurhash';
import { createTempDir } from '@/misc/create-temp.js';
import { AiService } from '@/core/AiService.js';
import { LoggerService } from '@/core/LoggerService.js';
@ -452,7 +452,7 @@ export class FileInfoService {
}
/**
* Calculate average color of image
* Calculate blurhash string of image
*/
@bindThis
private getBlurhash(path: string, type: string): Promise<string> {
@ -467,7 +467,7 @@ export class FileInfoService {
let hash;
try {
hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
} catch (e) {
return reject(e);
}

View File

@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
import type { ModerationLogPayloads } from '@/types.js';
import { moderationLogTypes } from '@/types.js';
@Injectable()
export class ModerationLogService {

View File

@ -509,7 +509,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const meta = await this.metaService.fetch();
this.notesChart.update(note, true);
if (meta.enableChartsForRemoteUser || (user.host == null)) {
if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) {
this.perUserNotesChart.update(user, note, true);
}

View File

@ -92,7 +92,7 @@ export class NoteDeleteService {
this.deliverToConcerned(user, note, content);
}
// also deliever delete activity to cascaded notes
// also deliver delete activity to cascaded notes
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
for (const cascadingNote of federatedLocalCascadingNotes) {
if (!cascadingNote.user) continue;

View File

@ -35,7 +35,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
}
@bindThis

View File

@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { ModuleRef } from '@nestjs/core';
import { reversiUpdateKeys } from 'misskey-js';
import * as Reversi from 'misskey-reversi';
import { IsNull, LessThan, MoreThan } from 'typeorm';
import type {
@ -399,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] {
if (typeof key !== 'string') return false;
return (reversiUpdateKeys as string[]).includes(key);
}
@bindThis
public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] {
switch (key) {
case 'map':
return Array.isArray(value) && value.every(row => typeof row === 'string');
case 'bw':
return typeof value === 'string' && ['random', '1', '2'].includes(value);
case 'isLlotheo':
return typeof value === 'boolean';
case 'canPutEverywhere':
return typeof value === 'boolean';
case 'loopedBoard':
return typeof value === 'boolean';
case 'timeLimitForEachTurn':
return typeof value === 'number' && value >= 0;
default:
return false;
}
}
@bindThis
public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) {
const game = await this.get(gameId);
if (game == null) throw new Error('game not found');
if (game.isStarted) return;
@ -407,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
if ((game.user1Id === user.id) && game.user1Ready) return;
if ((game.user2Id === user.id) && game.user2Ready) return;
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
// TODO: より厳格なバリデーション
const updatedGame = {
...game,
[key]: value,

View File

@ -127,10 +127,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
this.redisForSub.on('message', this.onMessage);
}

View File

@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown {
) {
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
memoryCacheLifetime: 1000 * 60 * 60, // 1h
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),

View File

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull } from 'typeorm';
import type { FollowingsRepository } from '@/models/_.js';
import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class UserSuspendService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private userEntityService: UserEntityService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
) {
}
@bindThis
public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
await this.usersRepository.update(user.id, {
isSuspended: true,
});
this.moderationLogService.log(moderator, 'suspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.postSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
})();
}
@bindThis
public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> {
await this.usersRepository.update(user.id, {
isSuspended: false,
});
this.moderationLogService.log(moderator, 'unsuspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.postUnsuspend(user).catch(e => {});
})();
}
@bindThis
private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
this.followRequestsRepository.delete({
followeeId: user.id,
});
this.followRequestsRepository.delete({
followerId: user.id,
});
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
@ -58,7 +109,7 @@ export class UserSuspendService {
}
@bindThis
public async doPostUnsuspend(user: MiUser): Promise<void> {
private async postUnsuspend(user: MiUser): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) {
@ -86,4 +137,26 @@ export class UserSuspendService {
}
}
}
@bindThis
private async unFollowAll(follower: MiUser) {
const followings = await this.followingsRepository.find({
where: {
followerId: follower.id,
followeeId: Not(IsNull()),
},
});
const jobs: RelationshipJobData[] = [];
for (const following of followings) {
if (following.followeeId && following.followerId) {
jobs.push({
from: { id: following.followerId },
to: { id: following.followeeId },
silent: true,
});
}
}
this.queueService.createUnfollowJob(jobs);
}
}

View File

@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown {
private cacheService: CacheService,
private apPersonService: ApPersonService,
) {
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
}
@bindThis

View File

@ -6,6 +6,7 @@
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@ -180,7 +181,8 @@ export class ApRequestService {
* @param url URL to fetch
*/
@bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
@ -198,9 +200,54 @@ export class ApRequestService {
headers: req.request.headers,
}, {
throwErrorWhenResponseNotOk: true,
validators: [validateContentTypeSetAsActivityPub],
});
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
const contentType = res.headers.get('content-type');
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
const html = await res.text();
const window = new Window({
settings: {
disableJavaScriptEvaluation: true,
disableJavaScriptFileLoading: true,
disableCSSFileLoading: true,
disableComputedStyleRendering: true,
handleDisabledFileLoadingAsSuccess: true,
navigation: {
disableMainFrameNavigation: true,
disableChildFrameNavigation: true,
disableChildPageNavigation: true,
disableFallbackToSetURL: true,
},
timer: {
maxTimeout: 0,
maxIntervalTime: 0,
maxIntervalIterations: 0,
},
},
});
const document = window.document;
try {
document.documentElement.innerHTML = html;
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
const href = alternate.getAttribute('href');
if (href) {
return await this.signedGet(href, user, false);
}
}
} catch (e) {
// something went wrong parsing the HTML, ignore the whole thing
} finally {
window.close();
}
}
//#endregion
validateContentTypeSetAsActivityPub(res);
return await res.json();
}
}

View File

@ -78,9 +78,10 @@ export class ApNoteService {
@bindThis
public validateNote(object: IObject, uri: string): Error | null {
const expectHost = this.utilityService.extractDbHost(uri);
const apType = getApType(object);
if (!validPost.includes(getApType(object))) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`);
if (apType == null || !validPost.includes(apType)) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
}
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {

View File

@ -48,7 +48,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js';
import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { ApImageService } from './ApImageService.js';
import type { IActor, IObject } from '../type.js';
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
const nameLength = 128;
const summaryLength = 2048;
@ -296,6 +296,21 @@ export class ApPersonService implements OnModuleInit {
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
const [followingVisibility, followersVisibility] = await Promise.all(
[
this.isPublicCollection(person.following, resolver),
this.isPublicCollection(person.followers, resolver),
].map((p): Promise<'public' | 'private'> => p
.then(isPublic => isPublic ? 'public' : 'private')
.catch(err => {
if (!(err instanceof StatusError) || err.isRetryable) {
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
}
return 'private';
})
)
);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
@ -357,6 +372,8 @@ export class ApPersonService implements OnModuleInit {
description: _description,
url,
fields,
followingVisibility,
followersVisibility,
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
userHost: host,
@ -464,6 +481,23 @@ export class ApPersonService implements OnModuleInit {
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
const [followingVisibility, followersVisibility] = await Promise.all(
[
this.isPublicCollection(person.following, resolver),
this.isPublicCollection(person.followers, resolver),
].map((p): Promise<'public' | 'private' | undefined> => p
.then(isPublic => isPublic ? 'public' : 'private')
.catch(err => {
if (!(err instanceof StatusError) || err.isRetryable) {
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
// Do not update the visibiility on transient errors.
return undefined;
}
return 'private';
})
)
);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
@ -532,6 +566,8 @@ export class ApPersonService implements OnModuleInit {
url,
fields,
description: _description,
followingVisibility,
followersVisibility,
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
});
@ -703,4 +739,16 @@ export class ApPersonService implements OnModuleInit {
return 'ok';
}
@bindThis
private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> {
if (collection) {
const resolved = await resolver.resolveCollection(collection);
if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) {
return true;
}
}
return false;
}
}

View File

@ -60,11 +60,14 @@ export function getApId(value: string | IObject): string {
/**
* Get ActivityStreams Object type
*
* nullを返すようにしている
* 詳細: https://github.com/misskey-dev/misskey/issues/14239
*/
export function getApType(value: IObject): string {
export function getApType(value: IObject): string | null {
if (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error('cannot detect type');
return null;
}
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
@ -97,19 +100,23 @@ export interface IActivity extends IObject {
export interface ICollection extends IObject {
type: 'Collection';
totalItems: number;
items: ApObject;
first?: IObject | string;
items?: ApObject;
}
export interface IOrderedCollection extends IObject {
type: 'OrderedCollection';
totalItems: number;
orderedItems: ApObject;
first?: IObject | string;
orderedItems?: ApObject;
}
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
export const isPost = (object: IObject): object is IPost =>
validPost.includes(getApType(object));
export const isPost = (object: IObject): object is IPost => {
const type = getApType(object);
return type != null && validPost.includes(type);
};
export interface IPost extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
@ -156,8 +163,10 @@ export const isTombstone = (object: IObject): object is ITombstone =>
export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
export const isActor = (object: IObject): object is IActor =>
validActor.includes(getApType(object));
export const isActor = (object: IObject): object is IActor => {
const type = getApType(object);
return type != null && validActor.includes(type);
};
export interface IActor extends IObject {
type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
@ -240,12 +249,16 @@ export interface IKey extends IObject {
publicKeyPem: string | Buffer;
}
export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video'];
export interface IApDocument extends IObject {
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
}
export const isDocument = (object: IObject): object is IApDocument =>
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
export const isDocument = (object: IObject): object is IApDocument => {
const type = getApType(object);
return type != null && validDocumentTypes.includes(type);
};
export interface IApImage extends IApDocument {
type: 'Image';
@ -323,7 +336,10 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
export const isLike = (object: IObject): object is ILike => {
const type = getApType(object);
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
};
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';

View File

@ -65,21 +65,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
.setParameters(pubsubSubQuery.getParameters())
@ -88,7 +88,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
@ -96,7 +96,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()

View File

@ -54,10 +54,10 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
}
@bindThis
public async getChartUsers(span: 'hour' | 'day', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{
public async getChartUsers(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{
userId: string;
count: number;
}[]> {
return await this.getChartPv(span, amount, cursor, limit, offset);
return await this.getChartPv(span, amount, cursor, limit, offset, order);
}
}

View File

@ -734,7 +734,7 @@ export default abstract class Chart<T extends Schema> {
}
@bindThis
public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number): Promise<
public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number, order: 'ASC' | 'DESC'): Promise<
{
userId: string,
count: number,
@ -756,27 +756,13 @@ export default abstract class Chart<T extends Schema> {
new Error('not happen') as never;
// ログ取得
const logs = await repository.createQueryBuilder()
return await repository.createQueryBuilder()
.select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"')
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
.orderBy('___pv_visitor + ___upv_visitor + ___pv_user + ___upv_user', 'DESC')
.skip(offset)
.take(limit)
.getMany() as {
___pv_visitor: number,
___upv_visitor: number,
___pv_user: number,
___upv_user: number,
group: string,
}[];
const result = [] as {
userId: string,
count: number,
}[];
for (const row of logs) {
const userId = row.group;
const count = row.___pv_user + row.___upv_user + row.___pv_visitor + row.___upv_visitor;
result.push({ userId, count });
}
return result;
.groupBy('"userId"')
.orderBy('"count"', order)
.offset(offset)
.limit(limit)
.getRawMany<{ userId: string, count: number }>();
}
}

View File

@ -49,6 +49,7 @@ export class FlashEntityService {
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
});

View File

@ -63,8 +63,9 @@ export class InstanceEntityService {
@bindThis
public packMany(
instances: MiInstance[],
me?: { id: MiUser['id']; } | null | undefined,
) {
return Promise.all(instances.map(x => this.pack(x)));
return Promise.all(instances.map(x => this.pack(x, me)));
}
}

View File

@ -129,6 +129,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
maxFileSize: this.config.maxFileSize,
preferPopularUserFactor: instance.preferPopularUserFactor,
};

View File

@ -454,12 +454,12 @@ export class UserEntityService implements OnModuleInit {
}
const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
(profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount :
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.followersVisibility === 'public') || isMe ? user.followersCount :
(profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount :
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;

View File

@ -10,8 +10,9 @@
* The getter will return a .bind version of the function
* and memoize the result against a symbol on the instance
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bindThis(target: any, key: string, descriptor: any) {
let fn = descriptor.value;
const fn = descriptor.value;
if (typeof fn !== 'function') {
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
configurable: true,
get() {
// eslint-disable-next-line no-prototype-builtins
if (this === target.prototype || this.hasOwnProperty(key) ||
typeof fn !== 'function') {
if (this === target.prototype || this.hasOwnProperty(key)) {
return fn;
}
const boundFn = fn.bind(this);
Object.defineProperty(this, key, {
Reflect.defineProperty(this, key, {
value: boundFn,
configurable: true,
get() {
return boundFn;
},
set(value) {
fn = value;
delete this[key];
},
writable: true,
});
return boundFn;
},
set(value: any) {
fn = value;
},
};
}

View File

@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
export class RedisKVCache<T> {
private redisClient: Redis.Redis;
private name: string;
private lifetime: number;
private memoryCache: MemoryKVCache<T>;
private fetcher: (key: string) => Promise<T>;
private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T | undefined;
private readonly lifetime: number;
private readonly memoryCache: MemoryKVCache<T>;
private readonly fetcher: (key: string) => Promise<T>;
private readonly toRedisConverter: (value: T) => string;
private readonly fromRedisConverter: (value: string) => T | undefined;
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
lifetime: RedisKVCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisKVCache<T>['fetcher'];
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
}) {
this.redisClient = redisClient;
this.name = name;
constructor(
private redisClient: Redis.Redis,
private name: string,
opts: {
lifetime: RedisKVCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisKVCache<T>['fetcher'];
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
},
) {
this.lifetime = opts.lifetime;
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
@ -55,7 +55,13 @@ export class RedisKVCache<T> {
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
const value = this.fromRedisConverter(cached);
if (value !== undefined) {
this.memoryCache.set(key, value);
}
return value;
}
@bindThis
@ -66,6 +72,10 @@ export class RedisKVCache<T> {
/**
* fetcherを呼び出して結果をキャッシュ&
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
*/
@bindThis
public async fetch(key: string): Promise<T> {
@ -77,14 +87,14 @@ export class RedisKVCache<T> {
// Cache MISS
const value = await this.fetcher(key);
this.set(key, value);
await this.set(key, value);
return value;
}
@bindThis
public async refresh(key: string) {
const value = await this.fetcher(key);
this.set(key, value);
await this.set(key, value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
@ -101,23 +111,23 @@ 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;
private readonly lifetime: number;
private readonly memoryCache: MemorySingleCache<T>;
private readonly fetcher: () => Promise<T>;
private readonly toRedisConverter: (value: T) => string;
private readonly 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;
constructor(
private redisClient: Redis.Redis,
private name: string,
opts: {
lifetime: number;
memoryCacheLifetime: number;
fetcher: RedisSingleCache<T>['fetcher'];
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
},
) {
this.lifetime = opts.lifetime;
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
@ -149,7 +159,13 @@ export class RedisSingleCache<T> {
const cached = await this.redisClient.get(`singlecache:${this.name}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
const value = this.fromRedisConverter(cached);
if (value !== undefined) {
this.memoryCache.set(value);
}
return value;
}
@bindThis
@ -160,6 +176,10 @@ export class RedisSingleCache<T> {
/**
* fetcherを呼び出して結果をキャッシュ&
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
*/
@bindThis
public async fetch(): Promise<T> {
@ -171,14 +191,14 @@ export class RedisSingleCache<T> {
// Cache MISS
const value = await this.fetcher();
this.set(value);
await this.set(value);
return value;
}
@bindThis
public async refresh() {
const value = await this.fetcher();
this.set(value);
await this.set(value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
@ -187,22 +207,12 @@ export class RedisSingleCache<T> {
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class MemoryKVCache<T> {
/**
*
* @deprecated
*/
public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
private gcIntervalHandle: NodeJS.Timeout;
private readonly cache = new Map<string, { date: number; value: T; }>();
private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
this.gcIntervalHandle = setInterval(() => {
this.gc();
}, 1000 * 60 * 3);
}
constructor(
private readonly lifetime: number,
) {}
@bindThis
/**
@ -287,10 +297,14 @@ export class MemoryKVCache<T> {
@bindThis
public gc(): void {
const now = Date.now();
for (const [key, { date }] of this.cache.entries()) {
if ((now - date) > this.lifetime) {
this.cache.delete(key);
}
// The map is ordered from oldest to youngest.
// We can stop once we find an entry that's still active, because all following entries must *also* be active.
const age = now - date;
if (age < this.lifetime) break;
this.cache.delete(key);
}
}
@ -298,16 +312,19 @@ export class MemoryKVCache<T> {
public dispose(): void {
clearInterval(this.gcIntervalHandle);
}
public get entries() {
return this.cache.entries();
}
}
export class MemorySingleCache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
this.lifetime = lifetime;
}
constructor(
private lifetime: number,
) {}
@bindThis
public set(value: T): void {

View File

@ -144,7 +144,9 @@ export interface Schema extends OfSchema {
readonly type?: TypeStringef;
readonly nullable?: boolean;
readonly optional?: boolean;
readonly prefixItems?: ReadonlyArray<Schema>;
readonly items?: Schema;
readonly unevaluatedItems?: Schema | boolean;
readonly properties?: Obj;
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
readonly description?: string;
@ -198,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
type ArrayUnion<T> = T extends any ? Array<T> : never;
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
type ObjectSchemaTypeDef<p extends Schema> =
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
@ -232,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> =
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
never
) :
p['prefixItems'] extends ReadonlyArray<Schema> ? (
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
) :
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
any[]
) :

View File

@ -6,3 +6,7 @@
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
export type JsonObject = {[K in string]?: JsonValue};
export type JsonArray = JsonValue[];
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View File

@ -85,7 +85,7 @@ export type MiNotification = {
/**
* body
*/
customBody: string | null;
customBody: string;
/**
* header

View File

@ -44,6 +44,11 @@ export const packedFlashSchema = {
type: 'string',
optional: false, nullable: false,
},
visibility: {
type: 'string',
optional: false, nullable: false,
enum: ['private', 'public'],
},
likedCount: {
type: 'number',
optional: false, nullable: true,

View File

@ -258,6 +258,10 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
default: 'local',
},
maxFileSize: {
type: 'number',
optional: false, nullable: false,
},
},
} as const;

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import { notificationTypes } from '@/types.js';
const baseSchema = {
@ -294,6 +295,7 @@ export const packedNotificationSchema = {
achievement: {
type: 'string',
optional: false, nullable: false,
enum: ACHIEVEMENT_TYPES,
},
},
}, {
@ -311,11 +313,11 @@ export const packedNotificationSchema = {
},
header: {
type: 'string',
optional: false, nullable: false,
optional: false, nullable: true,
},
icon: {
type: 'string',
optional: false, nullable: false,
optional: false, nullable: true,
},
},
}, {

View File

@ -45,7 +45,7 @@ export class DeliverProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60);
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
}
@bindThis

View File

@ -134,7 +134,7 @@ export class NodeinfoServerService {
return document;
};
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m
fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(() => nodeinfo2(21));

View File

@ -199,9 +199,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
const [path] = await createTemp();
const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartData.file.truncated) {
cleanup();
reply.code(413);
reply.send();
return;
}
const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;

View File

@ -49,7 +49,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
fileSize: this.config.maxFileSize ?? 262144000,
fileSize: this.config.maxFileSize,
files: 1,
},
});

View File

@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown {
private cacheService: CacheService,
) {
this.appCache = new MemoryKVCache<MiApp>(Infinity);
this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w
}
@bindThis

View File

@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
export const meta = {
tags: ['admin'],
@ -33,9 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
private queueService: QueueService,
private userSuspendService: UserSuspendService,
private deleteAccoountService: DeleteAccountService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -48,22 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot delete a root account');
}
if (this.userEntityService.isLocalUser(user)) {
// 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(err => {});
this.queueService.createDeleteAccountJob(user, {
soft: false,
});
} else {
this.queueService.createDeleteAccountJob(user, {
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
});
}
await this.usersRepository.update(user.id, {
isDeleted: true,
});
await this.deleteAccoountService.deleteAccount(user);
});
}
}

View File

@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
],
},
prefixItems: [
{
type: 'string',
},
{
type: 'number',
},
],
unevaluatedItems: false,
},
example: [[
'example.com',

View File

@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
items: {
anyOf: [
{
type: 'string',
},
{
type: 'number',
},
],
},
prefixItems: [
{
type: 'string',
},
{
type: 'number',
},
],
unevaluatedItems: false,
},
example: [[
'example.com',

View File

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin', 'role'],
@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private metaService: MetaService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps) => {
super(meta, paramDef, async (ps, me) => {
const before = await this.metaService.fetch(true);
await this.metaService.update({
policies: ps.policies,
});
this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
const after = await this.metaService.fetch(true);
this.globalEventService.publishInternalEvent('policiesUpdated', after.policies);
this.moderationLogService.log(me, 'updateServerSettings', {
before: before.policies,
after: after.policies,
});
});
}
}

View File

@ -3,18 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull, Not } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { UsersRepository } from '@/models/_.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
@ -38,13 +32,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userSuspendService: UserSuspendService,
private roleService: RoleService,
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -57,42 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot suspend moderator account');
}
await this.usersRepository.update(user.id, {
isSuspended: true,
});
this.moderationLogService.log(me, 'suspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
(async () => {
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
})();
await this.userSuspendService.suspend(user, me);
});
}
@bindThis
private async unFollowAll(follower: MiUser) {
const followings = await this.followingsRepository.find({
where: {
followerId: follower.id,
followeeId: Not(IsNull()),
},
});
const jobs: RelationshipJobData[] = [];
for (const following of followings) {
if (following.followeeId && following.followerId) {
jobs.push({
from: { id: following.followerId },
to: { id: following.followeeId },
silent: true,
});
}
}
this.queueService.createUnfollowJob(jobs);
}
}

View File

@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js';
@ -33,7 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private usersRepository: UsersRepository,
private userSuspendService: UserSuspendService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
@ -42,17 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('user not found');
}
await this.usersRepository.update(user.id, {
isSuspended: false,
});
this.moderationLogService.log(me, 'unsuspend', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
this.userSuspendService.doPostUnsuspend(user);
await this.userSuspendService.unsuspend(user, me);
});
}
}

View File

@ -34,6 +34,12 @@ export const meta = {
code: 'TOO_MANY_ANTENNAS',
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
},
emptyKeyword: {
message: 'Either keywords or excludeKeywords is required.',
code: 'EMPTY_KEYWORD',
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
},
},
res: {
@ -87,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({

View File

@ -32,6 +32,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
emptyKeyword: {
message: 'Either keywords or excludeKeywords is required.',
code: 'EMPTY_KEYWORD',
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
},
},
res: {
@ -85,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new Error('either keywords or excludeKeywords is required.');
throw new ApiError(meta.errors.emptyKeyword);
}
}
// Fetch the antenna

View File

@ -170,7 +170,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const instances = await query.limit(ps.limit).offset(ps.offset).getMany();
return await this.instanceEntityService.packMany(instances);
return await this.instanceEntityService.packMany(instances, me);
});
}
}

View File

@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
return await awaitAll({
topSubInstances: this.instanceEntityService.packMany(topSubInstances),
topSubInstances: this.instanceEntityService.packMany(topSubInstances, me),
otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
topPubInstances: this.instanceEntityService.packMany(topPubInstances),
topPubInstances: this.instanceEntityService.packMany(topPubInstances, me),
otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
});
});

View File

@ -4,9 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { FlashsRepository } from '@/models/_.js';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
if (flash == null) {
throw new ApiError(meta.errors.noSuchFlash);
}
if (flash.userId !== me.id) {
if (!await this.roleService.isModerator(me) && flash.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.flashsRepository.delete(flash.id);
if (flash.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: flash.userId });
this.moderationLogService.log(me, 'deleteFlash', {
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
flash,
});
}
});
}
}

View File

@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository } from '@/models/_.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -22,6 +24,12 @@ export const meta = {
code: 'NO_SUCH_POST',
id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496',
},
},
} as const;
@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const post = await this.galleryPostsRepository.findOneBy({
id: ps.postId,
userId: me.id,
});
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
if (post == null) {
throw new ApiError(meta.errors.noSuchPost);
}
if (!await this.roleService.isModerator(me) && post.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.galleryPostsRepository.delete(post.id);
if (post.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: post.userId });
this.moderationLogService.log(me, 'deleteGalleryPost', {
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
post,
});
}
});
}
}

View File

@ -4,9 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { PagesRepository } from '@/models/_.js';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId !== me.id) {
if (!await this.roleService.isModerator(me) && page.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.pagesRepository.delete(page.id);
if (page.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: page.userId });
this.moderationLogService.log(me, 'deletePage', {
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
page,
});
}
});
}
}

View File

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const chartUsers: { userId: string; count: number; }[] = [];
if (ps.sort?.endsWith('pv')) {
await this.perUserPvChart.getChartUsers('day', 0, null, ps.limit, ps.offset).then(users => {
await this.perUserPvChart.getChartUsers('day', ps.sort === '+pv' ? 'DESC' : 'ASC', 0, null, ps.limit, ps.offset).then(users => {
chartUsers.push(...users);
});
}

View File

@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private followingEntityService: FollowingEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy(ps.userId != null
@ -93,23 +95,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.followersVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.followersVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (!isFollowing) {
if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) {
if (profile.followersVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.followersVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (!isFollowing) {
throw new ApiError(meta.errors.forbidden);
}
}
}
}

View File

@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -90,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private utilityService: UtilityService,
private followingEntityService: FollowingEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy(ps.userId != null
@ -102,23 +104,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.followingVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.followingVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (!isFollowing) {
if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) {
if (profile.followingVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.followingVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,
},
});
if (!isFollowing) {
throw new ApiError(meta.errors.forbidden);
}
}
}
}

View File

@ -14,11 +14,14 @@ import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { JsonObject } from '@/misc/json-value.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
const MAX_CHANNELS_PER_CONNECTION = 32;
/**
* Main stream connection
*/
@ -112,8 +115,6 @@ export default class Connection {
const { type, body } = obj;
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
switch (type) {
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
@ -154,7 +155,8 @@ export default class Connection {
}
@bindThis
private readNote(body: JsonObject) {
private readNote(body: JsonValue | undefined) {
if (!isJsonObject(body)) return;
const id = body.id;
const note = this.cachedNotes.find(n => n.id === id);
@ -166,7 +168,7 @@ export default class Connection {
}
@bindThis
private onReadNotification(payload: JsonObject) {
private onReadNotification(payload: JsonValue | undefined) {
this.notificationService.readAllNotification(this.user!.id);
}
@ -174,7 +176,8 @@ export default class Connection {
* 稿
*/
@bindThis
private onSubscribeNote(payload: JsonObject) {
private onSubscribeNote(payload: JsonValue | undefined) {
if (!isJsonObject(payload)) return;
if (!payload.id || typeof payload.id !== 'string') return;
const current = this.subscribingNotes[payload.id] ?? 0;
@ -190,7 +193,8 @@ export default class Connection {
* 稿
*/
@bindThis
private onUnsubscribeNote(payload: JsonObject) {
private onUnsubscribeNote(payload: JsonValue | undefined) {
if (!isJsonObject(payload)) return;
if (!payload.id || typeof payload.id !== 'string') return;
const current = this.subscribingNotes[payload.id];
@ -216,12 +220,13 @@ export default class Connection {
*
*/
@bindThis
private onChannelConnectRequested(payload: JsonObject) {
private onChannelConnectRequested(payload: JsonValue | undefined) {
if (!isJsonObject(payload)) return;
const { channel, id, params, pong } = payload;
if (typeof id !== 'string') return;
if (typeof channel !== 'string') return;
if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
if (typeof params !== 'undefined' && !isJsonObject(params)) return;
this.connectChannel(id, params, channel, pong ?? undefined);
}
@ -229,7 +234,8 @@ export default class Connection {
*
*/
@bindThis
private onChannelDisconnectRequested(payload: JsonObject) {
private onChannelDisconnectRequested(payload: JsonValue | undefined) {
if (!isJsonObject(payload)) return;
const { id } = payload;
if (typeof id !== 'string') return;
this.disconnectChannel(id);
@ -251,6 +257,10 @@ export default class Connection {
*/
@bindThis
public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) {
return;
}
const channelService = this.channelsService.getChannelService(channel);
if (channelService.requireCredential && this.user == null) {
@ -297,7 +307,8 @@ export default class Connection {
* @param data
*/
@bindThis
private onChannelMessageRequested(data: JsonObject) {
private onChannelMessageRequested(data: JsonValue | undefined) {
if (!isJsonObject(data)) return;
if (typeof data.id !== 'string') return;
if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return;

View File

@ -6,6 +6,7 @@
import Xev from 'xev';
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -36,7 +37,7 @@ class QueueStatsChannel extends Channel {
public onMessage(type: string, body: JsonValue) {
switch (type) {
case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (!isJsonObject(body)) return;
if (typeof body.id !== 'string') return;
if (typeof body.length !== 'number') return;
ev.once(`queueStatsLog:${body.id}`, statsLog => {

View File

@ -9,8 +9,10 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
import { reversiUpdateKeys } from 'misskey-js';
class ReversiGameChannel extends Channel {
public readonly chName = 'reversiGame';
@ -44,16 +46,17 @@ class ReversiGameChannel extends Channel {
this.ready(body);
break;
case 'updateSettings':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.key !== 'string') return;
if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
if (!isJsonObject(body)) return;
if (!this.reversiService.isValidReversiUpdateKey(body.key)) return;
if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return;
this.updateSettings(body.key, body.value);
break;
case 'cancel':
this.cancelGame();
break;
case 'putStone':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (!isJsonObject(body)) return;
if (typeof body.pos !== 'number') return;
if (typeof body.id !== 'string') return;
this.putStone(body.pos, body.id);
@ -63,7 +66,7 @@ class ReversiGameChannel extends Channel {
}
@bindThis
private async updateSettings(key: string, value: JsonObject) {
private async updateSettings<K extends typeof reversiUpdateKeys[number]>(key: K, value: MiReversiGame[K]) {
if (this.user == null) return;
this.reversiService.updateSettings(this.gameId!, this.user, key, value);

View File

@ -6,6 +6,7 @@
import Xev from 'xev';
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@ -36,7 +37,7 @@ class ServerStatsChannel extends Channel {
public onMessage(type: string, body: JsonValue) {
switch (type) {
case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (!isJsonObject(body)) return;
ev.once(`serverStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog);
});

View File

@ -61,7 +61,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
const viteOut = `${_dirname}/../../../../../built/_vite_/`;
const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
@Injectable()
@ -277,15 +278,22 @@ export class ClientServerService {
});
//#region vite assets
if (this.config.clientManifestExists) {
if (this.config.frontendEmbedManifestExists) {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
root: viteOut,
root: frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: frontendEmbedViteOut,
prefix: '/embed_vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
});
@ -296,6 +304,13 @@ export class ClientServerService {
prefix: '/vite',
rewritePrefix: '/vite',
});
const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
fastify.register(fastifyProxy, {
upstream: 'http://localhost:' + embedPort,
prefix: '/embed_vite',
rewritePrefix: '/embed_vite',
});
}
//#endregion
@ -425,6 +440,13 @@ export class ClientServerService {
// Manifest
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
// Embed Javascript
fastify.get('/embed.js', async (request, reply) => {
return await reply.sendFile('/embed.js', staticAssets, {
maxAge: ms('1 day'),
});
});
fastify.get('/robots.txt', async (request, reply) => {
return await reply.sendFile('/robots.txt', staticAssets);
});
@ -762,7 +784,7 @@ export class ClientServerService {
});
//#endregion
//region noindex pages
//#region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
@ -772,7 +794,20 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
//endregion
//#endregion
//#region embed pages
fastify.get('/embed/*', async (request, reply) => {
const meta = await this.metaService.fetch();
reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('base-embed', {
title: meta.name ?? 'Misskey',
...await this.generateCommonPugData(meta),
});
});
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
@ -787,6 +822,7 @@ export class ClientServerService {
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
});
});
//#endregion
fastify.get('/bios', async (request, reply) => {
return await reply.view('bios', {

View File

@ -0,0 +1,219 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED');
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE');
};
let forceError = localStorage.getItem('forceError');
if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return;
}
// パラメータに応じてsplashのスタイルを変更
const params = new URLSearchParams(location.search);
if (params.has('rounded') && params.get('rounded') === 'false') {
document.documentElement.classList.add('norounded');
}
if (params.has('border') && params.get('border') === 'false') {
document.documentElement.classList.add('noborder');
}
//#region Detect language & fetch translations
if (!localStorage.hasOwnProperty('locale')) {
const supportedLangs = LANGS;
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
if (lang == null) lang = 'en-US';
}
}
const metaRes = await window.fetch('/api/meta', {
method: 'POST',
body: JSON.stringify({}),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
});
if (metaRes.status !== 200) {
renderError('META_FETCH');
return;
}
const meta = await metaRes.json();
const v = meta.version;
if (v == null) {
renderError('META_FETCH_V');
return;
}
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
if (localRes.status === 200) {
localStorage.setItem('lang', lang);
localStorage.setItem('locale', await localRes.text());
localStorage.setItem('localeVersion', v);
} else {
renderError('LOCALE_FETCH');
return;
}
}
//#endregion
//#region Script
async function importAppScript() {
await import(`/embed_vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
renderError('APP_IMPORT');
});
}
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
if (document.readyState !== 'loading') {
importAppScript();
} else {
window.addEventListener('DOMContentLoaded', () => {
importAppScript();
});
}
//#endregion
async function addStyle(styleText) {
let css = document.createElement('style');
css.appendChild(document.createTextNode(styleText));
document.head.appendChild(css);
}
async function renderError(code) {
// Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<div class="message">読み込みに失敗しました</div>
<div class="submessage">Failed to initialize Misskey</div>
<div class="submessage">Error Code: ${code}</div>
<button onclick="location.reload(!0)">
<div>リロード</div>
<div><small>Reload</small></div>
</button>`;
addStyle(`
#misskey_app,
#splash {
display: none !important;
}
html,
body {
margin: 0;
}
body {
position: relative;
color: #dee7e4;
font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
line-height: 1.35;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 24px;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--radius, 12px);
border: 1px solid rgba(231, 255, 251, 0.14);
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #192320;
border-radius: var(--radius, 12px);
z-index: -1;
}
html.embed.norounded body,
html.embed.norounded body::before {
border-radius: 0;
}
html.embed.noborder body {
border: none;
}
.icon {
max-width: 60px;
width: 100%;
height: auto;
margin-bottom: 20px;
color: #dec340;
}
.message {
text-align: center;
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
.submessage {
text-align: center;
font-size: 90%;
margin-bottom: 7.5px;
}
.submessage:last-of-type {
margin-bottom: 20px;
}
button {
padding: 7px 14px;
min-width: 100px;
font-weight: 700;
font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
line-height: 1.35;
border-radius: 99rem;
background-color: #b4e900;
color: #192320;
border: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
button:hover {
background-color: #c6ff03;
}`);
}
})();

View File

@ -3,17 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* BOOT LOADER
* サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで以下の役割を持ちます
* - 翻訳ファイルをフェッチする
* - バージョンに基づいて適切なメインスクリプトを読み込む
* - キャッシュされたコンパイル済みテーマを適用する
* - クライアントの設定値に基づいて対応するHTMLクラス等を設定する
* テーマをこの段階で設定するのはメインスクリプトが読み込まれる間もテーマを適用したいためです
* : webpackは介さないためこのファイルではrequireやimportは使えません
*/
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@ -166,7 +155,7 @@
if (!errorsElement) {
document.body.innerHTML = `
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
@ -176,10 +165,10 @@
<span class="button-label-big">Reload / リロード</span>
</button>
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります</b></p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
<p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;">
<summary>Other options / その他のオプション</summary>
<a href="/flush">
@ -212,7 +201,7 @@
<summary>
<code>ERROR CODE: ${code}</code>
</summary>
<code>${JSON.stringify(details)}</code>`;
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
errorsElement.appendChild(detailsElement);
addStyle(`
* {
@ -320,6 +309,6 @@
#errorInfo {
width: 50%;
}
}`)
}`);
}
})();

View File

@ -47,6 +47,7 @@ html {
transform: translateY(70px);
color: var(--accent);
}
#splashSpinner > .spinner {
position: absolute;
top: 0;

View File

@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
html {
background-color: var(--bg);
color: var(--fg);
}
html.embed {
box-sizing: border-box;
background-color: transparent;
color-scheme: light dark;
max-width: 500px;
}
#splash {
position: fixed;
z-index: 10000;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: wait;
background-color: var(--bg);
opacity: 1;
transition: opacity 0.5s ease;
}
html.embed #splash {
box-sizing: border-box;
min-height: 300px;
border-radius: var(--radius, 12px);
border: 1px solid var(--divider, #e8e8e8);
}
html.embed.norounded #splash {
border-radius: 0;
}
html.embed.noborder #splash {
border: none;
}
#splashIcon {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 64px;
height: 64px;
pointer-events: none;
}
#splashSpinner {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
display: inline-block;
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
}
#splashSpinner > .spinner {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
#splashSpinner > .spinner.fg {
animation: splashSpinner 0.5s linear infinite;
}
@keyframes splashSpinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,67 @@
block vars
block loadClientEntry
- const entry = config.frontendEmbedEntry;
doctype html
html(class='embed')
head
meta(charset='utf-8')
meta(name='application-name' content='Misskey')
meta(name='referrer' content='origin')
meta(name='theme-color' content= themeColor || '#86b300')
meta(name='theme-color-orig' content= themeColor || '#86b300')
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='modulepreload' href=`/embed_vite/${entry.file}`)
if !config.frontendEmbedManifestExists
script(type="module" src="/embed_vite/@vite/client")
if Array.isArray(entry.css)
each href in entry.css
link(rel='stylesheet' href=`/embed_vite/${href}`)
title
block title
= title || 'Misskey'
block meta
meta(name='robots' content='noindex')
style
include ../style.embed.css
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson
script
include ../boot.embed.js
body
noscript: p
| JavaScriptを有効にしてください
br
| Please turn on your JavaScript
div#splash
img#splashIcon(src= icon || '/static-assets/splash.png')
div#splashSpinner
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
block content

View File

@ -1,7 +1,7 @@
block vars
block loadClientEntry
- const clientEntry = config.clientEntry;
- const entry = config.frontendEntry;
doctype html
@ -28,6 +28,7 @@ html
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(property='instance_url' content= instanceUrl)
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json')
@ -35,15 +36,13 @@ html
link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
link(rel='modulepreload' href=`/vite/${entry.file}`)
if !config.clientManifestExists
if !config.frontendManifestExists
script(type="module" src="/vite/@vite/client")
if Array.isArray(clientEntry.css)
each href in clientEntry.css
if Array.isArray(entry.css)
each href in entry.css
link(rel='stylesheet' href=`/vite/${href}`)
title
@ -69,7 +68,7 @@ html
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}";
var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson

View File

@ -96,6 +96,10 @@ export const moderationLogTypes = [
'createAbuseReportNotificationRecipient',
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
] as const;
export type ModerationLogPayloads = {
@ -314,6 +318,29 @@ export type ModerationLogPayloads = {
recipientId: string;
recipient: any;
};
deleteAccount: {
userId: string;
userUsername: string;
userHost: string | null;
};
deletePage: {
pageId: string;
pageUserId: string;
pageUserUsername: string;
page: any;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
flash: any;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
post: any;
};
};
export type Serialized<T> = {

View File

@ -228,6 +228,17 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
test('を作成する時キーワードが指定されていないとエラーになる', async () => {
await failedApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] },
user: alice
}, {
status: 400,
code: 'EMPTY_KEYWORD',
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a'
})
});
//#endregion
//#region 更新(antennas/update)
@ -255,6 +266,18 @@ describe('アンテナ', () => {
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
});
});
test('を変更する時キーワードが指定されていないとエラーになる', async () => {
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
await failedApiCall({
endpoint: 'antennas/update',
parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] },
user: alice
}, {
status: 400,
code: 'EMPTY_KEYWORD',
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4'
})
});
//#endregion
//#region 表示(antennas/show)

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