Compare commits

...

366 Commits

Author SHA1 Message Date
syuilo
e9165a0d95 13.11.0.beta-2 2023-04-06 11:32:14 +09:00
syuilo
73203a3d72 perf(backend): cache local custom emojis 2023-04-06 11:14:43 +09:00
syuilo
437de6417e Update CHANGELOG.md 2023-04-06 10:30:56 +09:00
syuilo
fd05826187 fix(ci): use redis 7 2023-04-06 10:30:06 +09:00
syuilo
bd142b8fe5 refactor 2023-04-06 10:12:30 +09:00
syuilo
384a3bed09 perf(backend): reduce db query 2023-04-06 10:09:44 +09:00
syuilo
3c04fef8d1 13.11.0.beta-1 2023-04-06 08:44:40 +09:00
totegamma
9629929977 fix misskey-dev/misskey-#10417 (#10418) 2023-04-06 08:37:18 +09:00
Ryu jongheon
e76d3e72db refactor(fronted): setup syntax to menubar of Classic UI (#10474) 2023-04-06 08:35:14 +09:00
syuilo
19349b930a 🎨 2023-04-06 08:23:00 +09:00
syuilo
10c3c15f06 Update CHANGELOG.md 2023-04-06 08:20:17 +09:00
Acid Chicken (硫酸鶏)
3b3f683f8c feat(#8149): respect nsfw settings on gallery list (#10481)
* feat(#8149): respect nsfw settings on gallery list

* ci(#10336): use pull_request

* test(#8149): add interaction tests

* test(#10336): use `waitFor`

* chore: transition
2023-04-06 08:19:49 +09:00
Acid Chicken (硫酸鶏)
516a791bf4 docs: thanks (#10487) 2023-04-06 06:12:29 +09:00
tamaina
2650a7a5b8 fix: 通知インジケータが光りっぱなしになる問題を修正 (#10483)
* fix(misskey-js): ストリームがstringで送信される場合があるのを修正

* pnpm run api

* force read notification

* fix competition
2023-04-06 06:11:59 +09:00
Acid Chicken (硫酸鶏)
712c60106a ci: fix syntax 2023-04-05 18:35:23 +00:00
Acid Chicken (硫酸鶏)
41aafdea00 test(#10336): stabilize MkError stories 2023-04-06 03:10:45 +09:00
syuilo
0bb882c9d4 🎨 2023-04-05 15:45:44 +09:00
syuilo
3f3eca7d3a Update CHANGELOG.md 2023-04-05 14:50:40 +09:00
xtexChooser
62efe2b4d5 fix: webhook delivery content-type (#9823) (#10458)
* fix: webhook delivery content-type (#9823)

* style: indent

* Update packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts

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

* style: trailing comma

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-04-05 14:49:58 +09:00
tamaina
6798effbab enhance(client): 投稿フォームをちょっといい感じに (#10442)
* .formラッパーを削除

* fix type of MkPostFormAttaches

* 🚀

* 🎨

* 🎨

* 🎨

* 🎨

* specifiedの時は連合なしをdisabledに

* ✌️

* set select default

* gap: 2px (max-width: 500px) / 4px

* wip

* ✌️

* 🎨

* fix maxTextLength

* 今後表示しない

* 🎨

* cache channel

* 🎨

* 連合なしにする

* use i18n.ts.neverShow

* ✌️

* refactor

* fix indent

* tweak

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2023-04-05 14:30:03 +09:00
syuilo
d739aeee32 fix(frontend): add missing import 2023-04-05 14:29:18 +09:00
syuilo
a093c373c4 fix 2023-04-05 13:57:22 +09:00
syuilo
288cce4a22 fix 2023-04-05 13:50:05 +09:00
syuilo
28647de196 enhance(backend): improve userkeypair cache 2023-04-05 12:10:40 +09:00
syuilo
cf6a302f8f add note 2023-04-05 12:08:18 +09:00
syuilo
1c3d9a5df0 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-05 10:21:13 +09:00
syuilo
f44504097c enhance(backend): improve cache 2023-04-05 10:21:10 +09:00
syuilo
625fed8838 enhance(backend): チャンネルの既読管理を削除
- 現状上手く機能していない
- パフォーマンス上の理由
- 実装するにしてももっと効率的な方法がある
2023-04-05 07:52:49 +09:00
Acid Chicken (硫酸鶏)
3f39fc90fc Merge pull request #10457 from na2na-p/fix/visibility-widening 2023-04-05 02:15:09 +09:00
Acid Chicken (硫酸鶏)
7bd0001e76 Merge branch 'develop' into fix/visibility-widening 2023-04-05 00:41:49 +09:00
syuilo
ecaf152b4a enhance(backend): improve cache 2023-04-04 17:32:09 +09:00
syuilo
7f3afac0a2 refactor(backend): rename Cache -> MemoryCache 2023-04-04 15:56:47 +09:00
Acid Chicken (硫酸鶏)
a5f76c064e Merge pull request #10466 from misskey-dev/storybook
build(#10336): upgrade Storybook to v7
2023-04-04 15:23:40 +09:00
Acid Chicken (硫酸鶏)
d6e04da20b style(#10336): update comment-out codes 2023-04-04 15:03:45 +09:00
Acid Chicken (硫酸鶏)
55652a7a18 build(#10336): bump storybook to v7 2023-04-04 15:01:55 +09:00
Acid Chicken (硫酸鶏)
7e845ebbc2 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-04-04 15:01:31 +09:00
syuilo
6954a6300c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-04 14:07:04 +09:00
syuilo
30d6992684 perf(backend): 通知をRedisに保存するように
Resolve #10168
2023-04-04 14:06:57 +09:00
tamaina
c25d89ef9c fix(client): fix prop type in MkOmit 2023-04-04 01:45:01 +00:00
Acid Chicken (硫酸鶏)
38d0b62167 build(#10336): Storybook & Chromatic & msw (#10365)
* build(#10336): init

* fix(#10336): invalid name conversion

* build(#10336): load locales and vite config

* refactor(#10336): remove unused imports

* build(#10336): separate definitions and generated codes

* refactor(#10336): remove hatches

* refactor(#10336): module semantics

* refactor(#10336): remove unused common preferences

* fix: typo

* build(#10336): mock assets

* build(#10336): impl `SatisfiesExpression`

* build(#10336): control themes

* refactor(#10336): semantics

* build(#10336): make .storybook as an individual TypeScript project

* style(#10336): use single quote

* build(#10336): avoid intrinsic component names

* chore: suppress linter

* style: typing

* build(#10336): update dependencies

* docs: note about Storybook

* build(#10336): sync

* build(#10336): full reload server on change

* chore: use defaultStore instead

* build(#10336): show popups on Story

* refactor(#10336): remove redundant div

* docs: fix

* build(#10336): interactions

* build(#10336): add an interaction test for `<MkA/>`

* build(#10336): bump storybook

* docs(#10336): mention to pre-build misskey-js

* build(#10336): write stories for `MkAcct`

* build(#10336): write stories for `MkAd`

* build(#10336): fix missing type definition

* build(#10336): use `toHaveTextContent`

* build(#10336): write some stories

* build(#10336): hide internal args

* build(#10336): generate `components/global` stories only

* build(#10336): write stories for `MkMisskeyFlavoredMarkdown`

* fix: conflict errors

* build(#10336): subcomponents on sidebar

* refactor: restore `SatisfiesExpression`

* docs(#10336): note development status

* build(#10336): use chokidar-cli

* docs(#10336): note chokidar-cli mode

* chore(#10336): untrack generated stories files

* fix: pointer handling

* build(#10336): finalize

* chore: add static option to `MkLoading`

* refactor(#10336): bind to local args

* fix: missing case

* revert: restore `SatisfiesExpression`

This reverts commit f246699f38.

* build(#10336): make storybook buildable

* build(#10336): staticify assets

* build(#10336): staticified directory structure

* build(#10336): normalize path for Windows

* ci(#10336): create actions

* build(#10336): ignore tsc errors

* build(#10336): ignore tsc errors

* build(#10336): missing dependencies

* build(#10336): missing dependencies

* build(#10336): use fast-glob

* fix: invalid lockfile

* ci(#10336): increase heap size

* build(#10336): use unpkg for storybook tabler icons

* build(#10336): use unpkg for storybook twemojis

* build(#10336): disable `ProfilePageCat`

* build(#10336): blur `MkA` before interaction ends

* ci(#10336): stabilize

* ci(#10336): fetch-depth

* build(#10336): isChromatic

* ci(#10336): notify on changes

* ci(#10336): fix typo

* ci(#10336): missing working directory

* ci(#10336): skip build

* ci(#10336): fix path

* build(#10336): fails on Windows

* build(#10336): available on Windows

* ci(#10336): disable animation on chromatic

* ci(#10336): add static option to `PageHeader.tabs`

* chore: void

* ci(#10336): change parameters

* docs(#10336): update CONTRIBUTING

* docs(#10336): note about meta overriding and etc.

* ci(#10336): use Chromatic for checks

* ci(#10336): use `pull_request` instead of `pull_request_target` for now

* ci(#10336): use `exitOnceUploaded`

* ci(#10336): reuse built storybook

* ci(#10336): back to `pull_request_target`

* chore: unused dependencies

* style(#10336): reduce prettier indents

* style: note about `TSSatisfiesExpression`
2023-04-04 09:38:34 +09:00
Acid Chicken (硫酸鶏)
b91d831e82 style: note about TSSatisfiesExpression 2023-04-03 20:55:24 +09:00
syuilo
8a0201fe9c add notes 2023-04-03 20:14:19 +09:00
A.Yamamoto
ce1fb06ce8 Merge branch 'develop' into fix/visibility-widening 2023-04-03 15:08:54 +09:00
Acid Chicken (硫酸鶏)
14591aa8f2 style(#10336): reduce prettier indents 2023-04-03 14:51:14 +09:00
Acid Chicken (硫酸鶏)
38b9fc4f25 chore: unused dependencies 2023-04-03 14:22:57 +09:00
syuilo
32149f5122 Update CHANGELOG.md 2023-04-03 14:11:08 +09:00
Acid Chicken (硫酸鶏)
211682d7ec ci(#10336): back to pull_request_target 2023-04-03 14:05:19 +09:00
Acid Chicken (硫酸鶏)
b3b64d8525 ci(#10336): reuse built storybook 2023-04-03 13:58:20 +09:00
Acid Chicken (硫酸鶏)
2acb655c0f ci(#10336): use exitOnceUploaded 2023-04-03 13:50:22 +09:00
Acid Chicken (硫酸鶏)
8d90e88e16 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-04-03 13:44:32 +09:00
Acid Chicken (硫酸鶏)
9b004248a9 ci(#10336): use pull_request instead of pull_request_target for now 2023-04-03 13:44:17 +09:00
Acid Chicken (硫酸鶏)
b303a80491 Merge branch 'storybook' of https://github.com/misskey-dev/misskey into storybook 2023-04-03 13:41:55 +09:00
Acid Chicken (硫酸鶏)
b1b871aebf ci(#10336): use Chromatic for checks 2023-04-03 13:41:33 +09:00
Acid Chicken (硫酸鶏)
58f3a2ee94 Merge pull request #10420 from misskey-dev/acid-chicken-patch-1
fix: #10413
2023-04-03 13:01:24 +09:00
Acid Chicken (硫酸鶏)
8a9847b06a Merge branch 'develop' into acid-chicken-patch-1 2023-04-03 13:01:15 +09:00
syuilo
da6b30c5dd Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-04-03 12:47:46 +09:00
nexryai
92ddebb3fd オブジェクトストレージの設定画面を分かりやすくする (#10456)
* enhance: Improved object storage configuration page

* Update ja-JP.yml

* Update CHANGELOG
2023-04-03 12:47:30 +09:00
syuilo
b53d6c7f8c perf(backend): store notes of an antenna to redis instead of postgresql
Resolve #10169
2023-04-03 12:11:16 +09:00
syuilo
c032dd1214 fix(frontend): tweak MkPagination behaviouyr 2023-04-03 11:50:17 +09:00
syuilo
4f9f625e65 perf(backend): cache timeline of a channel to redis 2023-04-03 11:49:58 +09:00
A.Yamamoto
a46bfc7c14 レビュー対応
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-04-03 11:24:19 +09:00
Acid Chicken (硫酸鶏)
4f7ba294d9 docs(#10336): note about meta overriding and etc. 2023-04-03 11:03:19 +09:00
Acid Chicken (硫酸鶏)
902d7c5538 docs(#10336): update CONTRIBUTING 2023-04-03 10:50:21 +09:00
na2na-p
3877b366b3 除外範囲考慮するように 2023-04-03 10:16:43 +09:00
Acid Chicken (硫酸鶏)
423a67fb06 Merge branch 'storybook' of https://github.com/misskey-dev/misskey into storybook 2023-04-03 03:03:51 +09:00
Acid Chicken (硫酸鶏)
3ab2a008f2 ci(#10336): change parameters 2023-04-03 03:03:43 +09:00
Acid Chicken (硫酸鶏)
a61fe9023f Merge branch 'develop' into storybook 2023-04-03 02:50:14 +09:00
Acid Chicken (硫酸鶏)
36e8988ff7 chore: void 2023-04-03 02:09:47 +09:00
tamaina
a0c3dede8e enhance(server): 動画サムネイルの縦の長さを最大422pxにする
Related to #10149
Related to #10450
2023-04-02 15:49:07 +00:00
Acid Chicken (硫酸鶏)
dfb6df018a ci(#10336): add static option to PageHeader.tabs 2023-04-02 23:42:04 +09:00
Acid Chicken (硫酸鶏)
d6b1e5be2f Merge branch 'storybook' of https://github.com/misskey-dev/misskey into storybook 2023-04-02 23:26:52 +09:00
Acid Chicken (硫酸鶏)
ddb1635a46 ci(#10336): disable animation on chromatic 2023-04-02 23:26:35 +09:00
Acid Chicken (硫酸鶏)
9946ba0f08 build(#10336): available on Windows 2023-04-02 22:54:52 +09:00
Acid Chicken (硫酸鶏)
29c63adfb4 build(#10336): fails on Windows 2023-04-02 22:53:05 +09:00
Acid Chicken (硫酸鶏)
d57a504cac ci(#10336): fix path 2023-04-02 22:14:59 +09:00
Acid Chicken (硫酸鶏)
e4f6fac460 ci(#10336): skip build 2023-04-02 22:10:25 +09:00
Acid Chicken (硫酸鶏)
f9693d1fad ci(#10336): missing working directory 2023-04-02 22:06:11 +09:00
Acid Chicken (硫酸鶏)
252fe24e8d ci(#10336): fix typo 2023-04-02 21:58:37 +09:00
Acid Chicken (硫酸鶏)
5143f3372c ci(#10336): notify on changes 2023-04-02 21:50:02 +09:00
Acid Chicken (硫酸鶏)
5ab02cbeea Merge branch 'storybook' of https://github.com/misskey-dev/misskey into storybook 2023-04-02 21:31:50 +09:00
Acid Chicken (硫酸鶏)
faf827a50a build(#10336): isChromatic 2023-04-02 21:31:28 +09:00
Acid Chicken (硫酸鶏)
a217344315 ci(#10336): fetch-depth 2023-04-02 17:00:20 +09:00
Acid Chicken (硫酸鶏)
388ecfb1ea ci(#10336): stabilize 2023-04-02 14:59:19 +09:00
Acid Chicken (硫酸鶏)
9982ead9e6 build(#10336): blur MkA before interaction ends 2023-04-02 14:33:32 +09:00
Acid Chicken (硫酸鶏)
903d0bd3e2 build(#10336): disable ProfilePageCat 2023-04-02 13:29:23 +09:00
Acid Chicken (硫酸鶏)
edeabac8f7 build(#10336): use unpkg for storybook twemojis 2023-04-01 23:04:08 +09:00
Acid Chicken (硫酸鶏)
3f4d93a287 build(#10336): use unpkg for storybook tabler icons 2023-04-01 22:49:50 +09:00
Acid Chicken (硫酸鶏)
b9289c397b ci(#10336): increase heap size 2023-04-01 22:34:36 +09:00
Acid Chicken (硫酸鶏)
97e7f7efcc fix: invalid lockfile 2023-04-01 22:28:41 +09:00
Acid Chicken (硫酸鶏)
fc76829eb4 build(#10336): use fast-glob 2023-04-01 22:22:07 +09:00
Acid Chicken (硫酸鶏)
8b30a0d641 build(#10336): missing dependencies 2023-04-01 21:57:31 +09:00
Acid Chicken (硫酸鶏)
97731705ba build(#10336): missing dependencies 2023-04-01 21:54:12 +09:00
Acid Chicken (硫酸鶏)
b0fa9f3809 build(#10336): ignore tsc errors 2023-04-01 21:47:31 +09:00
Acid Chicken (硫酸鶏)
20fb266a93 build(#10336): ignore tsc errors 2023-04-01 21:45:20 +09:00
Acid Chicken (硫酸鶏)
7256b1f8d3 ci(#10336): create actions 2023-04-01 21:42:38 +09:00
Acid Chicken (硫酸鶏)
0f334f900c build(#10336): normalize path for Windows 2023-04-01 18:35:03 +09:00
Acid Chicken (硫酸鶏)
88981e9d5b build(#10336): staticified directory structure 2023-04-01 18:22:15 +09:00
Acid Chicken (硫酸鶏)
ec8905f81c build(#10336): staticify assets 2023-04-01 18:18:11 +09:00
Acid Chicken (硫酸鶏)
65f20de821 build(#10336): make storybook buildable 2023-04-01 18:05:14 +09:00
Acid Chicken (硫酸鶏)
e7a15b5604 revert: restore SatisfiesExpression
This reverts commit f246699f38.
2023-04-01 17:58:44 +09:00
Acid Chicken (硫酸鶏)
4521e61ae2 fix: missing case 2023-04-01 17:54:30 +09:00
Acid Chicken (硫酸鶏)
f39b5be064 refactor(#10336): bind to local args 2023-04-01 16:35:05 +09:00
Acid Chicken (硫酸鶏)
0960d61ce2 chore: add static option to MkLoading 2023-04-01 16:31:55 +09:00
Acid Chicken (硫酸鶏)
1521bb088c build(#10336): finalize 2023-04-01 16:26:08 +09:00
Acid Chicken (硫酸鶏)
38b153ca94 fix: pointer handling 2023-04-01 14:27:13 +09:00
Acid Chicken (硫酸鶏)
f5fd59d656 chore(#10336): untrack generated stories files 2023-04-01 14:21:46 +09:00
Acid Chicken (硫酸鶏)
f7bc442ba5 docs(#10336): note chokidar-cli mode 2023-04-01 14:18:17 +09:00
Acid Chicken (硫酸鶏)
139dade62c build(#10336): use chokidar-cli 2023-04-01 14:17:52 +09:00
Acid Chicken (硫酸鶏)
3f1a87d1ad Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-04-01 14:11:56 +09:00
Acid Chicken (硫酸鶏)
f3a29cbb0d docs(#10336): note development status 2023-04-01 14:11:16 +09:00
Acid Chicken (硫酸鶏)
f246699f38 refactor: restore SatisfiesExpression 2023-04-01 14:04:48 +09:00
syuilo
bc5e457a63 Delete vue.d.ts 2023-04-01 14:04:40 +09:00
syuilo
9224b6635f refactor(frontend): remove $ts and $t 2023-04-01 14:01:57 +09:00
Acid Chicken (硫酸鶏)
5667f0f839 build(#10336): subcomponents on sidebar 2023-04-01 13:59:00 +09:00
Acid Chicken (硫酸鶏)
d300f4a87b fix: conflict errors 2023-04-01 13:58:35 +09:00
syuilo
f16a7cbcf2 refactor(frontend): remove $i 2023-04-01 13:52:07 +09:00
Acid Chicken (硫酸鶏)
c6d12c21db build(#10336): write stories for MkMisskeyFlavoredMarkdown 2023-04-01 13:47:59 +09:00
Acid Chicken (硫酸鶏)
2b831da1d1 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-04-01 13:46:54 +09:00
syuilo
7a8a756789 refactor(frontend): remove $instance 2023-04-01 13:46:04 +09:00
Acid Chicken (硫酸鶏)
a47f4b8955 build(#10336): generate components/global stories only 2023-04-01 13:45:31 +09:00
syuilo
c823cbe63b refactor(frontend): remove $store 2023-04-01 13:42:40 +09:00
Acid Chicken (硫酸鶏)
5605b03ce4 build(#10336): hide internal args 2023-04-01 12:18:42 +09:00
Acid Chicken (硫酸鶏)
671ce6a531 build(#10336): write some stories 2023-04-01 04:20:38 +09:00
Acid Chicken (硫酸鶏)
14977ed2b7 fix: the avatar in the title bar is clipped 2023-03-31 07:47:03 +00:00
Acid Chicken (硫酸鶏)
13a406b6cf Merge branch 'develop' into acid-chicken-patch-1 2023-03-31 16:41:55 +09:00
syuilo
a9f0bea60c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-03-31 16:41:29 +09:00
syuilo
152247bfda refactor: remove autobind-decorator dep 2023-03-31 16:41:27 +09:00
tamaina
6a4d56c81c fix(client): set overflow: clip to page header title container
#10413
2023-03-31 07:38:28 +00:00
Acid Chicken (硫酸鶏)
85f50f3875 build(#10336): use toHaveTextContent 2023-03-31 16:36:43 +09:00
Acid Chicken (硫酸鶏)
558d73b21a build(#10336): fix missing type definition 2023-03-31 16:34:54 +09:00
Acid Chicken (硫酸鶏)
bf9846a269 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-03-31 15:54:18 +09:00
Acid Chicken (硫酸鶏)
70fc25aac1 build(#10336): write stories for MkAd 2023-03-31 15:52:39 +09:00
syuilo
9bc5d52e41 feat: チャンネルにノートをピン留めできるように
Resolve #7740
2023-03-31 15:01:56 +09:00
syuilo
f0a70a70c3 refactor 2023-03-31 14:14:30 +09:00
syuilo
3cb0cc7989 feat: チャンネルをお気に入りに登録できるように
Resolve #10097
2023-03-31 11:30:27 +09:00
syuilo
5d94062581 lint fix 2023-03-31 09:20:52 +09:00
syuilo
a23b25bff7 tweak misskey-js eslintrc 2023-03-31 09:18:58 +09:00
Acid Chicken (硫酸鶏)
98fd6b5879 build(#10336): write stories for MkAcct 2023-03-31 01:01:53 +09:00
Acid Chicken (硫酸鶏)
5393436403 docs(#10336): mention to pre-build misskey-js 2023-03-30 23:15:29 +09:00
Acid Chicken (硫酸鶏)
d1c5556724 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-03-30 23:02:38 +09:00
Acid Chicken (硫酸鶏)
3db0147392 build(#10336): bump storybook 2023-03-30 23:00:01 +09:00
tamaina
26068a3a8f use workspace:*, use pnpm@8.1.0 2023-03-30 13:01:00 +00:00
tamaina
0398787520 fix misskey-js/tsconfig.json (add dom lib) 2023-03-30 12:52:51 +00:00
tamaina
dccf03ffc2 Fix misskey-js/tsconfig.json
Fix #10441
2023-03-30 12:51:24 +00:00
syuilo
6d78162323 tweak misskey-js config 2023-03-30 14:00:51 +09:00
syuilo
74e0824b1d Revert "Update api-misskey-js.yml"
This reverts commit 231b54a889.
2023-03-30 13:56:48 +09:00
syuilo
231b54a889 Update api-misskey-js.yml 2023-03-30 13:55:17 +09:00
syuilo
d7fbef0a55 specify .js 2023-03-30 13:49:56 +09:00
syuilo
ea6f9f1e7d tweak misskey-js config 2023-03-30 13:46:25 +09:00
syuilo
ffbe896f28 enhance(client): ノートのリアクションを大きく表示するオプションを追加 2023-03-30 12:49:23 +09:00
syuilo
e9769c937b misskey-jsのビルド設定を修正 2023-03-30 12:30:59 +09:00
syuilo
e03d5a86a4 lint fix 2023-03-30 12:16:03 +09:00
syuilo
bb2b33f2c9 update deps 2023-03-30 12:03:04 +09:00
syuilo
0fa9f945cf Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-03-30 11:48:35 +09:00
Acid Chicken (硫酸鶏)
5c80ad8fe9 Merge branch 'develop' into acid-chicken-patch-1 2023-03-30 11:03:51 +09:00
Kagami Sascha Rosylight
cee1d5e2d0 chore: integrate misskey-js as a workspace item (git subtree) (#10409)
* Additional changes for the merge

* api-misskey-js
2023-03-30 09:33:19 +09:00
Acid Chicken (硫酸鶏)
49d4c538fe fix: typo 2023-03-29 23:37:43 +09:00
syuilo
c1d9e23171 enhance(client): 検索ページでURLを入力した際に照会したときと同等の挙動をするように 2023-03-29 17:22:34 +09:00
Acid Chicken (硫酸鶏)
8963e36aa2 docs: note change logs 2023-03-27 05:25:32 +00:00
Acid Chicken (硫酸鶏)
4d3f0be419 chore: respect the animation preference 2023-03-27 05:16:39 +00:00
Acid Chicken (硫酸鶏)
07cfab0489 fix: add tight state to cat ears for workarounds 2023-03-26 09:36:46 +00:00
Acid Chicken (硫酸鶏)
453574644c fix: add workaround for cat ears on iOS Safari 2023-03-26 09:25:51 +00:00
syuilo
a529b0e5a3 New Crowdin updates (#10407)
* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Polish)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)
2023-03-26 17:09:38 +09:00
Acid Chicken (硫酸鶏)
bc6d576a4e build(#10336): add an interaction test for <MkA/> 2023-03-25 23:35:47 +09:00
Acid Chicken (硫酸鶏)
356fd72603 build(#10336): interactions 2023-03-25 23:32:47 +09:00
Acid Chicken (硫酸鶏)
225a8e11a9 docs: fix 2023-03-25 20:19:20 +09:00
syuilo
4a0995a175 Update CHANGELOG.md 2023-03-25 18:48:11 +09:00
syuilo
6fe12b52fd Update ja-JP.yml 2023-03-25 18:47:11 +09:00
YS
31cc188bbd regionが空文字列のときはundefinedとする (#10410) 2023-03-25 18:45:14 +09:00
Kagami Sascha Rosylight
772e05e835 Revert "Subtree merged in packages/misskey-js"
This reverts commit 3b524f32bf, reversing
changes made to 5be54451a8.
2023-03-25 08:20:41 +01:00
Kagami Sascha Rosylight
abdb43c440 Revert "Additional changes for the merge"
This reverts commit efb5e1d1cc.
2023-03-25 08:20:34 +01:00
Kagami Sascha Rosylight
efb5e1d1cc Additional changes for the merge 2023-03-25 08:13:30 +01:00
Kagami Sascha Rosylight
3b524f32bf Subtree merged in packages/misskey-js 2023-03-25 08:09:24 +01:00
Acid Chicken (硫酸鶏)
899c6241c3 refactor(#10336): remove redundant div 2023-03-25 12:23:28 +09:00
Acid Chicken (硫酸鶏)
8ca9621d42 build(#10336): show popups on Story 2023-03-25 12:21:16 +09:00
Acid Chicken (硫酸鶏)
b56d173f8a chore: use defaultStore instead 2023-03-25 12:20:49 +09:00
Acid Chicken (硫酸鶏)
2bce064ada build(#10336): full reload server on change 2023-03-25 11:34:17 +09:00
syuilo
5be54451a8 Update CHANGELOG.md 2023-03-25 08:44:26 +09:00
Acid Chicken (硫酸鶏)
8332c03bd8 build(#10336): sync 2023-03-25 04:00:09 +09:00
Acid Chicken (硫酸鶏)
723cff0205 Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook 2023-03-24 23:25:56 +09:00
Acid Chicken (硫酸鶏)
bd7cc6ac38 docs: note about Storybook 2023-03-24 23:22:49 +09:00
Acid Chicken (硫酸鶏)
c2d8759812 build(#10336): update dependencies 2023-03-23 16:44:41 +09:00
Acid Chicken (硫酸鶏)
bf5fff879f style: typing 2023-03-22 00:48:11 +09:00
Acid Chicken (硫酸鶏)
1d0ca7eecf chore: suppress linter 2023-03-22 00:25:17 +09:00
Acid Chicken (硫酸鶏)
5ced8aef27 build(#10336): avoid intrinsic component names 2023-03-21 13:12:30 +09:00
Acid Chicken (硫酸鶏)
49f8bb4974 style(#10336): use single quote 2023-03-21 13:05:40 +09:00
Acid Chicken (硫酸鶏)
cc3fbefc83 build(#10336): make .storybook as an individual TypeScript project 2023-03-21 13:01:40 +09:00
Acid Chicken (硫酸鶏)
bf527cd6e7 refactor(#10336): semantics 2023-03-21 11:58:58 +09:00
Acid Chicken (硫酸鶏)
0ac4d744fd build(#10336): control themes 2023-03-21 02:05:00 +09:00
Acid Chicken (硫酸鶏)
bdbbb92ff6 build(#10336): impl SatisfiesExpression 2023-03-20 23:07:13 +09:00
Acid Chicken (硫酸鶏)
2eecb8e876 build(#10336): mock assets 2023-03-20 22:12:11 +09:00
Acid Chicken (硫酸鶏)
2ec608fdba fix: typo 2023-03-20 16:34:49 +09:00
Acid Chicken (硫酸鶏)
ca7d2081a2 refactor(#10336): remove unused common preferences 2023-03-20 16:29:36 +09:00
Acid Chicken (硫酸鶏)
3907cb51d6 refactor(#10336): module semantics 2023-03-20 16:27:40 +09:00
Acid Chicken (硫酸鶏)
be7b71a2f5 refactor(#10336): remove hatches 2023-03-20 16:15:03 +09:00
Acid Chicken (硫酸鶏)
6e21147ff6 build(#10336): separate definitions and generated codes 2023-03-20 16:13:07 +09:00
Acid Chicken (硫酸鶏)
a19c560e7b refactor(#10336): remove unused imports 2023-03-20 14:58:24 +09:00
Acid Chicken (硫酸鶏)
0dcb99df19 build(#10336): load locales and vite config 2023-03-20 14:56:34 +09:00
Acid Chicken (硫酸鶏)
91e0e498eb fix(#10336): invalid name conversion 2023-03-19 22:31:18 +09:00
Acid Chicken (硫酸鶏)
1dfcca7b9c build(#10336): init 2023-03-19 22:25:06 +09:00
日高 凌
13a2d16eab feat: add type of gallery (#55)
* update GalleryPost type

* update gallery/featured type

* update gallery/popular type

* update gallery/posts type

* update gallery/posts/create type

* update gallery/posts/like type

* update gallery/posts/show type

* update gallery/posts/unlike type

* update gallery/posts/update type
2023-02-11 07:55:29 +09:00
syuilo
4922cb92e0 0.0.15 2023-02-01 11:32:55 +09:00
syuilo
cc6a2670c8 Update misskey-js.api.md 2023-02-01 11:32:12 +09:00
果物リン
4f62f6f288 application/json の content-type header をデフォルトで付与する (#52) 2023-01-17 06:47:46 +09:00
Kagami Sascha Rosylight
fc921d8c1d enhance: add missing InstanceMetadata fields (#50) 2023-01-10 00:00:49 +09:00
Francis Dinh
c89374c321 fix: Add comment property to DriveFile (#46)
* Add comment property to DriveFile

Some components of Misskey like the image viewer use this property, but it hasn't been defined in the type definition itself.

* Make DriveFile.comment nullable

There's a few places in the client code that checks for a null comment,
suggesting this field may be set to null.
2022-09-17 21:44:41 +09:00
syuilo
6a87f4ade9 Update .eslintrc.js 2022-09-11 01:59:02 +09:00
syuilo
c17c42fa05 Update consts.ts 2022-03-06 15:58:44 +09:00
syuilo
5531a1fdf2 NoteUpdatedEvent に unreacted を追加
Fix #43
2022-02-03 02:24:42 +09:00
syuilo
78ee600752 Update .eslintrc.js 2022-01-31 21:29:29 +09:00
syuilo
bc9525c665 Update .eslintrc.js 2022-01-31 21:25:31 +09:00
syuilo
b41c1fe3af 0.0.14 2022-01-30 11:31:19 +09:00
nullobsi
84dffdf510 fix (#42) 2022-01-30 11:27:43 +09:00
syuilo
8ab9b92f60 0.0.13 2022-01-14 01:33:48 +09:00
syuilo
287f595239 Resolve #38 2022-01-14 01:33:42 +09:00
syuilo
cbce1242b9 0.0.12 2022-01-02 02:12:38 +09:00
syuilo
f3640add23 fix streaming query bug 2022-01-02 02:11:35 +09:00
syuilo
a05f47d5cc improve test 2022-01-02 02:11:25 +09:00
syuilo
6c990f3ce7 0.0.11 2022-01-01 21:54:41 +09:00
syuilo
a4b14d54f2 update copyright 2022-01-01 21:53:36 +09:00
syuilo
2c079a9608 fix test 2022-01-01 21:52:46 +09:00
syuilo
233ffe46d5 update deps 2022-01-01 21:52:42 +09:00
Sym
231ac8e323 UserDetailed, MeDetailed (#34)
* UserDetailed, MeDetailed

* npm run api

* remote: lastFetchedAt

* Update misskey-js.api.md

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2022-01-01 21:49:31 +09:00
tamaina
755189f6f3 Add missing main stream types (#36)
* Add missing main stream types

* changelog, fix

* update api

* fix

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2021-12-29 14:54:10 +09:00
syuilo
d2b634c349 fix lint 2021-12-28 00:45:47 +09:00
syuilo
4ff49b0a3f Update .eslintrc.js 2021-12-28 00:34:20 +09:00
syuilo
2bf63b158c Update .eslintrc.js 2021-12-11 22:31:57 +09:00
syuilo
de81437248 Fix #31 2021-11-20 11:47:19 +09:00
syuilo
043ffe3a2e 0.0.10 2021-11-11 22:49:25 +09:00
syuilo
43d9149c8a add consts 2021-11-11 22:49:16 +09:00
syuilo
f08a840d7d 0.0.9 2021-11-11 22:00:14 +09:00
syuilo
714753d001 add list of api permission, Update Note type 2021-11-11 21:59:50 +09:00
syuilo
7f3943f794 0.0.8 2021-10-17 17:52:27 +09:00
syuilo
80ef07222d Update misskey-js.api.md 2021-10-17 17:52:24 +09:00
syuilo
510a1408a2 update Note type 2021-10-17 17:51:19 +09:00
Nya Candy
34afa60aff feat: add type definition for messagingMessage event to main stream channel (#30)
* add: type definition for messagingMessage event

* update: changelog
2021-10-17 14:03:43 +09:00
syuilo
71f35d90c5 2021-10-17 00:00:40 +09:00
syuilo
8ba56df54e Update README.md 2021-10-16 23:57:02 +09:00
syuilo
227e603194 Update misskey-js.api.md 2021-10-16 23:49:50 +09:00
syuilo
4021c3c61a 0.0.7 2021-10-16 23:47:37 +09:00
syuilo
d549879cd4 update type definition 2021-10-16 23:46:12 +09:00
tamaina
b7fd9e8c55 fix: apiでネイティブfetchを格納する際に無名関数でラップするように (#29)
* fix fetch

* add comment

* 表現を変更
2021-09-21 19:45:07 +09:00
tamaina
f278390012 fix: Notificationsの型を修正 (#27)
* Update entities.ts

* Update entities.ts

* extract

* fix app define
extract

* appの定義を修正

* 改行コード修正

* add changelog

* fix

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2021-09-21 02:50:35 +09:00
tamaina
fa98dabeec fix: MessagingMessageの型を修正 (#28)
* fix messaging

* add changelog
2021-09-21 02:49:31 +09:00
syuilo
1243e6a655 Create config.yml 2021-08-28 18:01:59 +09:00
syuilo
7a148e9ffa update contribution guides 2021-08-28 15:42:07 +09:00
syuilo
a526f75a3e Update CONTRIBUTING.en.md 2021-08-28 15:35:16 +09:00
syuilo
b1b90185c4 Update CONTRIBUTING.md 2021-08-28 15:29:40 +09:00
syuilo
ab391870ed Update CONTRIBUTING.en.md 2021-08-28 15:28:08 +09:00
syuilo
3e6ad67d59 Update CONTRIBUTING.en.md 2021-08-28 15:26:14 +09:00
syuilo
39668a21d7 Update CONTRIBUTING.en.md 2021-08-28 15:23:08 +09:00
syuilo
85eb00b0a6 Update CONTRIBUTING.en.md 2021-08-28 15:18:11 +09:00
syuilo
0c61d052d2 update contribution guide 2021-08-28 15:13:23 +09:00
syuilo
e6e339cc3e Update CONTRIBUTING.md 2021-08-24 12:56:32 +09:00
syuilo
8ea124b999 Create CODE_OF_CONDUCT.md 2021-08-23 23:42:32 +09:00
syuilo
7ee3b4536c Create CONTRIBUTING.md 2021-08-23 23:38:58 +09:00
syuilo
99fa6e3cd0 Update api.yml 2021-07-30 13:03:30 +09:00
syuilo
32ff71a67d introduce api-extractor 2021-07-30 13:01:52 +09:00
syuilo
2ffff0fa61 ci: use npm ci to improve CI performance 2021-07-30 12:56:31 +09:00
syuilo
51ca78d496 Update .eslintrc.js 2021-07-30 01:18:44 +09:00
syuilo
677ed40b40 introduce eslint 2021-07-30 01:18:19 +09:00
syuilo
0616534af7 Update pull_request_template.md 2021-07-30 01:17:57 +09:00
syuilo
7a41dc2a7a Create CHANGELOG.md 2021-07-27 01:44:58 +09:00
syuilo
65f0b195a2 Create pull_request_template.md 2021-07-27 00:01:26 +09:00
syuilo
170e84360c Update README.md 2021-07-13 22:08:55 +09:00
syuilo
42b611de9b Update README.md 2021-07-11 19:34:35 +09:00
syuilo
015d169423 Update README.md 2021-07-11 19:33:21 +09:00
syuilo
56958d2397 Update README.md 2021-07-11 19:29:04 +09:00
syuilo
b11c00ccdc Update README.md 2021-07-11 19:18:27 +09:00
syuilo
dfc8a09f44 fix typo 2021-07-10 23:20:56 +09:00
syuilo
127a1320a8 Update README.md 2021-07-07 23:23:34 +09:00
syuilo
4e93f2f360 Update deps 2021-07-05 20:19:55 +09:00
syuilo
76d0a9cc5d refactor 2021-07-02 22:48:07 +09:00
syuilo
99dc38c6e0 0.0.6 2021-07-02 19:13:46 +09:00
syuilo
fe1c8b882a Resolve #1 2021-07-02 19:13:18 +09:00
syuilo
0acaa2f3b0 Improve type definiyion 2021-06-29 21:41:25 +09:00
syuilo
78861f8a31 add test 2021-06-28 18:34:25 +09:00
syuilo
05a902f3a5 improve type definitions 2021-06-28 18:34:20 +09:00
syuilo
0bab055441 refactor 2021-06-28 18:34:10 +09:00
syuilo
adb168dfdc add test 2021-06-28 17:57:15 +09:00
syuilo
8bc7daa3f3 refactor 2021-06-28 17:49:25 +09:00
syuilo
b6df8cc1f5 Add tests 2021-06-27 21:17:42 +09:00
syuilo
b95775041c Improve type 2021-06-27 21:17:38 +09:00
syuilo
36bfaeba12 0.0.5 2021-06-26 02:04:46 +09:00
syuilo
e40f0800a0 refactor 2021-06-26 02:00:22 +09:00
syuilo
154e0fe94a add tests 2021-06-25 09:58:35 +09:00
syuilo
c6285944a1 codecov integration 2021-06-25 09:46:40 +09:00
syuilo
037f68858d fix type 2021-06-25 09:12:54 +09:00
syuilo
873f24618c Update deps 2021-06-25 09:12:18 +09:00
syuilo
17c9f6eace Update type definitions 2021-06-25 00:00:17 +09:00
syuilo
2a84e5034e Update type definitions 2021-06-24 02:28:44 +09:00
syuilo
bc3b3bb1c0 Update type definition 2021-06-23 13:47:51 +09:00
syuilo
b729000687 Add test 2021-06-22 19:07:09 +09:00
syuilo
64cf49c378 Update type definition 2021-06-21 17:56:11 +09:00
syuilo
f894d978df Improve type definitions 2021-06-20 00:43:29 +09:00
syuilo
ce12fc2b67 refactor 2021-06-19 23:07:08 +09:00
syuilo
091ef2e240 fix connection close logic 2021-06-12 23:01:48 +09:00
syuilo
1d17516aa8 Add CI status badge 2021-06-12 22:50:45 +09:00
syuilo
b5a01217b1 rename github action 2021-06-12 22:47:11 +09:00
syuilo
b9056a9fd9 Create ci.yml 2021-06-12 22:42:44 +09:00
syuilo
aa9182bd50 0.0.4 2021-06-08 11:32:09 +09:00
syuilo
c75ff55bbc fix typescript config 2021-06-08 11:31:53 +09:00
syuilo
1e7219b043 Update type definitions 2021-06-05 15:26:54 +09:00
syuilo
5431f4fb86 0.0.3 2021-06-01 14:24:44 +09:00
syuilo
1cdf9e19a0 fix type 2021-06-01 14:17:05 +09:00
syuilo
5e6e1084cf Update test 2021-05-31 23:03:08 +09:00
syuilo
a1f2dd7846 Improve type 2021-05-31 23:00:48 +09:00
syuilo
b2549d98f9 Update streaming.ts 2021-05-31 11:28:20 +09:00
syuilo
b1934d95f2 Improve type definition 2021-05-31 11:28:17 +09:00
syuilo
d9fbddc26c 0.0.2 2021-05-27 15:23:52 +09:00
syuilo
b818da3b1c Update endpoints.ts 2021-05-27 15:19:21 +09:00
syuilo
509dc30ae6 Update endpoints.ts 2021-05-27 15:14:57 +09:00
syuilo
8c08be47cb update type definitions 2021-05-27 15:05:30 +09:00
syuilo
691f0d07b9 Update README.md 2021-05-23 17:17:14 +09:00
syuilo
a61507e971 Update README.md 2021-05-23 17:16:14 +09:00
syuilo
70aaf34094 0.0.1 2021-05-23 17:15:15 +09:00
marihachi
d311f74349 Add WebSocket option for Stream (#21) 2021-05-23 16:53:11 +09:00
marihachi
893fa691a7 add FetchLike (#19) 2021-05-23 16:42:27 +09:00
marihachi
39270c782a fix TODO type 2021-05-23 14:56:38 +09:00
marihachi
adae7cf2ec fix TODO type 2021-05-23 14:48:42 +09:00
syuilo
6ae5f76250 resolve #17 2021-05-23 13:34:36 +09:00
syuilo
ca655c0628 Update endpoints.ts 2021-05-23 12:47:34 +09:00
syuilo
8d3f9d7e34 ✌️ 2021-05-23 12:32:58 +09:00
syuilo
e7eac5baa7 update test 2021-05-23 12:22:25 +09:00
syuilo
08f673802f fix test 2021-05-23 12:19:39 +09:00
syuilo
bdb592539e update api api 2021-05-23 12:15:28 +09:00
marihachi
3557741f13 Append null to TODO type 2021-05-22 16:36:37 +09:00
marihachi
9d89314afa update userlist endpoints (#16) 2021-05-21 13:55:39 +09:00
marihachi
83b86c745d update endpoints (#15)
* update endpoints

* add endpoints
* endpoint
* endpoints

* update announcements endpoint

* replace to DateString

* update antennas endpoints

* update app endpoints

* update auth endpoints

* update entity: Antenna

* admin/delete-all-files-of-a-user

* fix

* admin/delete-logs

* update endpoints
* admin/ad/delete
* admin/announcements/delete

* update endpoints
* admin/federation/delete-all-files
* clips/delete

* update endpoints
* drive/files/delete
* drive/folders/delete
* gallery/posts/delete
* i/delete-account
* mute/delete

* update endpoints
* notes/favorites/delete
* notes/reactions/delete
* notes/watching/delete
* users/groups/delete
* users/lists/delete

* specifical id

* update antennas/notes endpoint
2021-05-21 12:27:13 +09:00
syuilo
c6088a1cb3 add test 2021-05-19 14:01:04 +09:00
syuilo
88c1874943 add comment 2021-05-18 03:27:03 +09:00
marihachi
ec50d657bb add endpoints (#14) 2021-05-18 00:08:33 +09:00
syuilo
e8c856924c ✌️ 2021-05-18 00:07:17 +09:00
syuilo
e1317ce098 test 2021-05-17 23:55:13 +09:00
syuilo
a385bd0227 add test 2021-05-17 19:57:04 +09:00
syuilo
99276028ae Typed stream (#13)
* Update streaming.ts

* Update streaming.ts

* wip
2021-05-17 19:50:31 +09:00
syuilo
d7d02cd2bc Update README.md 2021-05-17 00:07:01 +09:00
syuilo
bd52ff905f nannka 2021-05-16 23:42:06 +09:00
syuilo
49a655f78e Update README.md 2021-05-16 22:23:23 +09:00
syuilo
c27340eddc Update README.md 2021-05-16 18:33:08 +09:00
syuilo
57c6e7f823 Update README.md 2021-05-16 18:30:42 +09:00
syuilo
6c938905f9 やった 2021-05-16 18:27:21 +09:00
syuilo
767df5967a tab 2021-05-16 18:22:19 +09:00
syuilo
2f8f6431d1 ✌️ 2021-05-16 18:18:45 +09:00
syuilo
aace7a1900 add streaming test 2021-05-16 18:12:18 +09:00
syuilo
00bbd9c7bb Update package-lock.json 2021-05-16 18:12:12 +09:00
syuilo
1bd75619dd provide api error detection function 2021-05-16 12:59:52 +09:00
syuilo
c7d5da4ec5 define APIError 2021-05-16 11:25:03 +09:00
syuilo
1cfa30b982 rename 2021-05-16 11:19:51 +09:00
syuilo
f5fcc130bf export entity types 2021-05-16 11:16:32 +09:00
marihachi
c7bfdea17b misc (#10) 2021-05-16 11:12:29 +09:00
marihachi
460e23c2e7 fix fetch mock (#9) 2021-05-16 11:11:05 +09:00
marihachi
a4d5faa789 Rename render acct (#5) 2021-05-15 17:42:48 +09:00
marihachi
148730dac3 Add endpoints (#7)
* Add endpoints

* endpoint

* endpoint: stats

* endpoint
* reset-password
* server-info

* endpoint
* announcements
* drive
* page-push

* page endpoint

* page id
2021-05-15 14:25:18 +09:00
syuilo
e43a8bf88a Merge pull request #2 from misskey-dev/work/project-1
Add ignore config
2021-05-15 13:37:09 +09:00
marihachi
502ccea980 Add ignore config 2021-05-15 01:53:41 +09:00
syuilo
deab5bce91 refactor 2021-05-14 23:49:09 +09:00
syuilo
6dc01cd109 wip 2021-05-14 13:49:40 +09:00
syuilo
eacb5fea9f wip 2021-05-14 12:56:43 +09:00
syuilo
92c170d540 Update README.md 2021-05-14 12:00:58 +09:00
syuilo
32b0679472 i want you 2021-05-14 12:00:10 +09:00
syuilo
71d89fb33e Update README.md 2021-05-14 11:54:41 +09:00
syuilo
81289762bc rename 2021-05-14 11:54:37 +09:00
syuilo
cb0e275db9 wip 2021-05-14 11:46:39 +09:00
syuilo
b53a6bfe0c ✌️ 2021-05-12 23:29:17 +09:00
syuilo
8d7e6ee769 Initial commit 2021-05-11 14:14:30 +09:00
334 changed files with 20796 additions and 6741 deletions

36
.github/workflows/api-misskey-js.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: API report (misskey.js)
on: [push, pull_request]
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm --filter misskey-js build
- name: Check files
run: ls packages/misskey-js/built
- name: API report
run: pnpm --filter misskey-js api-prod
- name: Show report
if: always()
run: cat packages/misskey-js/temp/misskey-js.api.md

View File

@@ -36,6 +36,7 @@ jobs:
- backend
- frontend
- sw
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:
@@ -61,6 +62,7 @@ jobs:
matrix:
workspace:
- backend
- misskey-js
steps:
- uses: actions/checkout@v3.3.0
with:

58
.github/workflows/storybook.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Storybook
on:
push:
branches:
- master
- develop
pull_request:
branches-ignore:
- l10n_develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
run_install: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
env:
NODE_OPTIONS: "--max_old_space_size=7168"
- name: Publish to Chromatic
id: chromatic
uses: chromaui/action@v1
with:
exitOnceUploaded: true
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
storybookBuildDir: storybook-static
workingDir: packages/frontend
- name: Compare on Chromatic
if: github.event_name == 'pull_request_target'
run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: storybook
path: packages/frontend/storybook-static

View File

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

View File

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

52
.github/workflows/test-misskey-js.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test (misskey.js)
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build
run: pnpm --filter misskey-js build
- name: Test
run: pnpm --filter misskey-js test
env:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

2
.gitignore vendored
View File

@@ -55,6 +55,8 @@ api-docs.json
.DS_Store
/files
ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
# blender backups
*.blend1

View File

@@ -5,14 +5,50 @@
-
### Client
-
-
### Server
-
-->
## 13.10.3 (unreleased)
## 13.x.x (unreleased)
### NOTE
- Redis 7.xが必要です
### General
- チャンネルをお気に入りに登録できるように
- チャンネルにノートをピン留めできるように
### Client
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
- オブジェクトストレージの設定画面を分かりやすく
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
### Server
- サーバーの全体的なパフォーマンスを向上
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
- 通知に関する全体的なパフォーマンスを向上
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
## 13.10.3
### Changes
- オブジェクトストレージのリージョン指定が必須になりました
- リージョンの指定の無いサービスは us-east-1 を設定してください
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
- e.g. ~/aws/config, AWS_REGION
### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加

View File

@@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Storybook
Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run
#### Universal
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash
pnpm --filter misskey-js build
```
##### Run
```bash
pnpm --filter frontend storybook-dev
```
### Usage
When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
```ts
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-duplicates */
import { StoryObj } from '@storybook/vue3';
import MyComponent from './MyComponent.vue';
export const Default = {
render(args) {
return {
components: {
MyComponent,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MyComponent v-bind="props" />',
};
},
args: {
foo: 'bar',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAvatar>;
```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
```ts
import MyComponent from './MyComponent.vue';
void MyComponent;
```
You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
```ts
export const argTypes = {
scale: {
control: {
type: 'range',
min: 1,
max: 4,
},
};
```
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
```ts
import { rest } from 'msw';
export const handlers = [
rest.post('/api/notes/timeline', (req, res, ctx) => {
return res(
ctx.json([]),
);
}),
];
```
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Notes
### How to resolve conflictions occurred at pnpm-lock.yaml?

View File

@@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output

View File

@@ -54,6 +54,17 @@ With Misskey's built in drive, you get cloud storage right in your social media,
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
## Sponsors
<div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div>
## Thanks
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" width="117" height="30" alt="Docker" /></a>
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.

View File

@@ -460,7 +460,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil"
native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren"
@@ -980,6 +980,9 @@ drivecleaner: "Drive-Reiniger"
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
_achievements:
earnedAt: "Freigeschaltet am"
_types:
@@ -1877,6 +1880,17 @@ _drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
followed: "Wenn dir jemand folgt"
note: "Wenn du eine Notiz schickst"
reply: "Wenn du eine Antwort erhältst"
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@@ -960,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録する"
exploreOtherServers: "他のサーバーを探す"
letsLookAtTimeline: "タイムラインを見てみる"
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
disableFederationConfirm: "連合なしにしますか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
disableFederationOk: "連合なしにする"
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿"
@@ -983,6 +986,8 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
_achievements:
earnedAt: "獲得日時"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
selectList: "Wybierz listę"
selectChannel: "Wybierz kanał"
selectAntenna: "Wybierz Antennę"
selectWidget: "Wybierz widżet"
editWidgets: "Edytuj widżety"
@@ -149,6 +150,7 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
addAccount: "Dodaj konto"
reloadAccountsList: "Odśwież listę kont"
loginFailed: "Nie udało się zalogować"
showOnRemote: "Zobacz na zdalnej instancji"
general: "Ogólne"
@@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
youHaveNoLists: "Nie masz żadnej listy"
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
proxyAccount: "Konto proxy"
proxyAccountDescription: "Opis konta pełnomocniczego"
host: "Host"
selectUser: "Wybierz użytkownika"
recipient: "Odbiorca"
@@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
startMessaging: "Rozpocznij czat"
nUsersRead: "przeczytano przez {n}"
agreeTo: "Wyrażam zgodę na {0}"
agreeBelow: "Zaakceptuj poniżej"
tos: "Regulamin"
start: "Rozpocznij"
home: "Strona główna"
@@ -385,13 +389,19 @@ about: "Informacje"
aboutMisskey: "O Misskey"
administrator: "Admin"
token: "Token"
2fa: "Klucz 2FA "
totp: "Klucz aplikacji uwierzytelniającej (totp)"
totpDescription: "Opis klucza czasowego"
moderator: "Moderator"
moderation: "Moderacja"
nUsersMentioned: "{n} wspomnianych użytkowników"
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
securityKey: "Klucz bezpieczeństwa"
lastUsed: "Ostatnio używane"
lastUsedAt: "Ostatnio używane w"
unregister: "Cofnij rejestrację"
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
resetPassword: "Zresetuj hasło"
newPasswordIs: "Nowe hasło to „{password}”"
reduceUiAnimation: "Ogranicz animacje w UI"
@@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
deleteAllFiles: "Usuń wszystkie pliki"
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
removeAllFollowing: "Przestań obserwować"
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
userSuspended: "To konto zostało zawieszone."
userSilenced: "Ten użytkownik został wyciszony."
yourAccountSuspendedTitle: "To konto jest zawieszone"
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
tokenRevoked: "Token odrzucony"
tokenRevokedDescription: "Opis odrzuconego tokena"
accountDeleted: "Konto usunięte"
accountDeletedDescription: "Opis konta usuniętego"
menu: "Menu"
divider: "Rozdzielacz"
addItem: "Dodaj element"
@@ -548,7 +563,9 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie"
plugins: "Wtyczki"
preferencesBackups: "Kopia zapasowa ustawień"
deck: "Tablica"
undeck: "oddkouj"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
width: "Szerokość"
@@ -815,6 +832,8 @@ tenMinutes: "10 minut"
oneHour: "1 godzina"
oneDay: "1 dzień"
oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
file: "Pliki"
recommended: "Zalecane"
check: "Zweryfikuj"

View File

@@ -980,6 +980,9 @@ drivecleaner: "网盘整理"
retryAllQueuesNow: "立刻重试所有队列"
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
_achievements:
earnedAt: "达成时间"
_types:
@@ -1276,6 +1279,8 @@ _role:
followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于"
notesLessThanOrEq: "帖子数在~以下"
notesMoreThanOrEq: "帖子数在~以上"
and: "符合以下全部条件"
or: "符合以下任一条件"
not: "不符合以下任何条件"
@@ -1875,6 +1880,17 @@ _drivecleaner:
orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
createWebhook: "创建 Webhook"
name: "名称"
secret: "密钥"
events: "何时运行Webhook"
active: "已启用"
_events:
follow: "关注时"
followed: "被关注时"
note: "发布贴文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消"
noThankYou: "現在不要"
enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了"
renotedBy: "{user} 轉了"
noNotes: "無貼文。"
noNotifications: "沒有通知"
instance: "實例"
@@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "轉成功"
renoted: "轉成功"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉之前已經轉過的內容。"
cantReRenote: "無法轉之前已經轉過的內容。"
quote: "引用"
inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用"
@@ -980,6 +980,9 @@ drivecleaner: "雲端硬碟清掃器"
retryAllQueuesNow: "立刻重試所有佇列"
retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
_achievements:
earnedAt: "獲得日期"
_types:
@@ -1096,7 +1099,7 @@ _achievements:
title: "有備而來"
description: "設定了個人檔案"
_markedAsCat:
title: "我是貓"
title: "吾輩乃貓是也"
description: "已將帳戶設定為貓"
flavor: "還沒有名字。"
_following1:
@@ -1276,6 +1279,8 @@ _role:
followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上"
notesLessThanOrEq: "發布數在~以下"
notesMoreThanOrEq: "發布數在~以上"
and: "~和~"
or: "~或~"
not: "~否"
@@ -1875,6 +1880,17 @@ _drivecleaner:
orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings:
createWebhook: "建立 Webhook"
name: "名稱"
secret: "秘密"
events: "什麼時候運行Webhook"
active: "已啟用"
_events:
follow: "當你追隨時"
followed: "當被追隨時"
note: "當發布貼文時"
reply: "當收到回覆時"
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "13.10.3",
"version": "13.11.0.beta-2",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@7.29.3",
"packageManager": "pnpm@8.1.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@@ -50,16 +50,16 @@
"gulp-replace": "1.1.4",
"gulp-terser": "2.1.0",
"js-yaml": "4.1.0",
"typescript": "4.9.5"
"typescript": "5.0.2"
},
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"cross-env": "7.0.3",
"cypress": "12.7.0",
"eslint": "8.35.0",
"cypress": "12.9.0",
"eslint": "8.37.0",
"start-server-and-test": "2.0.0"
},
"optionalDependencies": {

View File

@@ -0,0 +1,21 @@
export class channelFavorite1680228513388 {
name = 'channelFavorite1680228513388'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
await queryRunner.query(`DROP TABLE "channel_favorite"`);
}
}

View File

@@ -0,0 +1,11 @@
export class channelNotePining1680238118084 {
name = 'channelNotePining1680238118084'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" ADD "pinnedNoteIds" character varying(128) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "pinnedNoteIds"`);
}
}

View File

@@ -0,0 +1,10 @@
export class cleanup1680491187535 {
name = 'cleanup1680491187535'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "antenna_note" `);
}
async down(queryRunner) {
}
}

View File

@@ -0,0 +1,11 @@
export class cleanup1680582195041 {
name = 'cleanup1680582195041'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "notification" `);
}
async down(queryRunner) {
}
}

View File

@@ -23,41 +23,41 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11",
"@swc/core-darwin-arm64": "^1.3.38",
"@swc/core-darwin-x64": "^1.3.38",
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
"@swc/core-linux-arm64-gnu": "^1.3.38",
"@swc/core-linux-arm64-musl": "^1.3.38",
"@swc/core-linux-x64-gnu": "^1.3.38",
"@swc/core-linux-x64-musl": "^1.3.38",
"@swc/core-win32-arm64-msvc": "^1.3.38",
"@swc/core-win32-ia32-msvc": "^1.3.38",
"@swc/core-win32-x64-msvc": "^1.3.38",
"@swc/core-darwin-arm64": "^1.3.42",
"@swc/core-darwin-x64": "^1.3.42",
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
"@swc/core-linux-arm64-gnu": "^1.3.42",
"@swc/core-linux-arm64-musl": "^1.3.42",
"@swc/core-linux-x64-gnu": "^1.3.42",
"@swc/core-linux-x64-musl": "^1.3.42",
"@swc/core-win32-arm64-msvc": "^1.3.42",
"@swc/core-win32-ia32-msvc": "^1.3.42",
"@swc/core-win32-x64-msvc": "^1.3.42",
"@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.294.0",
"@aws-sdk/lib-storage": "^3.294.0",
"@aws-sdk/node-http-handler": "^3.292.0",
"@aws-sdk/client-s3": "3.301.0",
"@aws-sdk/lib-storage": "3.301.0",
"@aws-sdk/node-http-handler": "3.296.0",
"@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.0.2",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0",
"@fastify/http-proxy": "8.4.0",
"@fastify/multipart": "7.4.2",
"@fastify/cors": "8.2.1",
"@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9",
"@nestjs/core": "9.3.9",
"@nestjs/testing": "9.3.9",
"@nestjs/common": "9.3.12",
"@nestjs/core": "9.3.12",
"@nestjs/testing": "9.3.12",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.38",
"@swc/core": "1.3.42",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@@ -76,7 +76,7 @@
"date-fns": "2.29.3",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"fastify": "4.14.1",
"fastify": "4.15.0",
"feed": "4.2.2",
"file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2",
@@ -88,21 +88,21 @@
"ip-cidr": "3.1.0",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "21.1.0",
"jsdom": "21.1.1",
"json5": "2.2.3",
"jsonld": "8.1.1",
"jsrsasign": "10.6.1",
"jsrsasign": "10.7.0",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"misskey-js": "0.0.15",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.0",
"node-fetch": "3.3.1",
"nodemailer": "6.9.1",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"os-utils": "0.0.14",
"otpauth": "^9.0.2",
"otpauth": "9.1.1",
"parse5": "7.1.2",
"pg": "8.10.0",
"private-ip": "3.0.0",
@@ -125,7 +125,7 @@
"sanitize-html": "2.10.0",
"seedrandom": "3.0.5",
"semver": "7.3.8",
"sharp": "0.31.3",
"sharp": "0.32.0",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@@ -133,25 +133,25 @@
"systeminformation": "5.17.12",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.3",
"tsconfig-paths": "4.1.2",
"tsc-alias": "1.8.5",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typescript": "4.9.5",
"typescript": "5.0.2",
"ulid": "2.3.0",
"unzipper": "0.10.11",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.5.0",
"websocket": "1.0.34",
"ws": "8.12.1",
"ws": "8.13.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.5.0",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
"@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0",
"@types/cbor": "6.0.0",
@@ -160,13 +160,13 @@
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10",
"@types/jest": "29.4.0",
"@types/jest": "29.5.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.0",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/node": "18.15.0",
"@types/node": "18.15.11",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@@ -178,7 +178,7 @@
"@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.1",
"@types/sanitize-html": "2.9.0",
"@types/semver": "7.3.13",
"@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2",
@@ -190,11 +190,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3",
"eslint": "8.35.0",
"eslint": "8.37.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.5.0",

View File

@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: Antenna[];
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
// 通知しない設定になっているか、自分自身の投稿なら既読にする
const read = !antenna.notify || (antenna.userId === noteUser.id);
this.antennaNotesRepository.insert({
id: this.idService.genId(),
antennaId: antenna.id,
noteId: note.id,
read: read,
});
this.redisClient.xadd(
`antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
where: {
muterId: antenna.userId,
},
select: ['muteeId'],
});
// Copy
const _note: Note = {
...note,
};
if (note.replyId != null) {
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
}
if (note.renoteId != null) {
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
}
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}
// 2秒経っても既読にならなかったら通知
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
}
}
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View File

@@ -0,0 +1,172 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class CacheService implements OnApplicationShutdown {
public userByIdCache: MemoryKVCache<User>;
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Set<string>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new MemoryKVCache<User>(Infinity);
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
});
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findUserById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -38,9 +38,9 @@ import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
import { UserBlockingService } from './UserBlockingService.js';
import { UserCacheService } from './UserCacheService.js';
import { CacheService } from './CacheService.js';
import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js';
@@ -159,9 +159,9 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService };
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
@@ -282,9 +282,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
TwoFactorAuthenticationService,
UserBlockingService,
UserCacheService,
CacheService,
UserFollowingService,
UserKeypairStoreService,
UserKeypairService,
UserListService,
UserMutingService,
UserSuspendService,
@@ -399,9 +399,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$TwoFactorAuthenticationService,
$UserBlockingService,
$UserCacheService,
$CacheService,
$UserFollowingService,
$UserKeypairStoreService,
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSuspendService,
@@ -517,9 +517,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService,
TwoFactorAuthenticationService,
UserBlockingService,
UserCacheService,
CacheService,
UserFollowingService,
UserKeypairStoreService,
UserKeypairService,
UserListService,
UserMutingService,
UserSuspendService,
@@ -633,9 +633,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService,
$TwoFactorAuthenticationService,
$UserBlockingService,
$UserCacheService,
$CacheService,
$UserFollowingService,
$UserKeypairStoreService,
$UserKeypairService,
$UserListService,
$UserMutingService,
$UserSuspendService,

View File

@@ -1,24 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js';
import type { EmojisRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
@Injectable()
export class CustomEmojiService {
private cache: KVCache<Emoji | null>;
private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config)
private config: Config,
@@ -32,9 +36,16 @@ export class CustomEmojiService {
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) {
this.cache = new KVCache<Emoji | null>(1000 * 60 * 60 * 12);
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(value.values()),
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
});
}
@bindThis
@@ -60,7 +71,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
await this.db.queryResultCache?.remove(['meta_emojis']);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
@@ -70,6 +81,146 @@ export class CustomEmojiService {
return emoji;
}
@bindThis
public async update(id: Emoji['id'], data: {
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
}): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
});
this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
}
@bindThis
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(aliases))],
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
aliases: aliases,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
category: category,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async delete(id: Emoji['id']) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
}
@bindThis
public async deleteBulk(ids: Emoji['id'][]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
}
@bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
@@ -84,7 +235,7 @@ export class CustomEmojiService {
}
@bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
@@ -143,30 +294,6 @@ export class CustomEmojiService {
return res;
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/**
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
*/

View File

@@ -36,8 +36,5 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, {
isDeleted: true,
});
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
@Injectable()
export class FederatedInstanceService {
private cache: KVCache<Instance>;
private cache: MemoryKVCache<Instance>;
constructor(
@Inject(DI.instancesRepository)
@@ -18,7 +18,7 @@ export class FederatedInstanceService {
private utilityService: UtilityService,
private idService: IdService,
) {
this.cache = new KVCache<Instance>(1000 * 60 * 60);
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
}
@bindThis

View File

@@ -14,7 +14,6 @@ import type {
MainStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
UserStreamTypes,
} from '@/server/api/stream/types.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
@@ -49,11 +48,6 @@ export class GlobalEventService {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishUserEvent<K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void {
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { genAid } from '@/misc/id/aid.js';
import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js';
@@ -32,4 +32,17 @@ export class IdService {
default: throw new Error('unrecognized id generation method');
}
}
@bindThis
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
default: throw new Error('unrecognized id generation method');
}
}
}

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
private cache: KVCache<LocalUser>;
private cache: MemorySingleCache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@@ -19,12 +19,12 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
this.cache = new KVCache<LocalUser>(Infinity);
this.cache = new MemorySingleCache<LocalUser>(Infinity);
}
@bindThis
public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(null);
const cached = this.cache.get();
if (cached) return cached;
const user = await this.usersRepository.findOneBy({
@@ -33,11 +33,11 @@ export class InstanceActorService {
}) as LocalUser | undefined;
if (user) {
this.cache.set(null, user);
this.cache.set(user);
return user;
} else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
this.cache.set(null, created);
this.cache.set(created);
return created;
}
}

View File

@@ -1,6 +1,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@@ -19,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { KVCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -46,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
if (data.channel) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ },
@@ -461,7 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.incNotesCountOfUser(user);
// Word mute
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
where: {
enableWordMute: true,
},
@@ -490,18 +502,6 @@ export class NoteCreateService implements OnApplicationShutdown {
});
}
// Channel
if (note.channelId) {
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
for (const following of followings) {
this.noteReadService.insertNoteUnread(following.followerId, note, {
isSpecified: false,
isMentioned: false,
});
}
});
}
if (data.reply) {
this.saveReply(data.reply, note);
}

View File

@@ -1,28 +1,20 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@@ -32,21 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
) {
}
@@ -57,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
@@ -77,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId,
};
@@ -95,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
}
if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, () => { /* aborted, ignore it */ });
}
@@ -105,23 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
public async read(
userId: User['id'],
notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
},
): Promise<void> {
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
@@ -129,25 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note);
}
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
// TODO: ↓まとめてクエリしたい
@@ -171,49 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
});
this.noteUnreadsRepository.countBy({
userId: userId,
noteChannelId: Not(IsNull()),
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
this.notificationService.readNotificationByQuery(userId, {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});
}
}

View File

@@ -1,8 +1,9 @@
import { setTimeout } from 'node:timers/promises';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -11,21 +12,22 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@@ -34,54 +36,36 @@ export class NotificationService implements OnApplicationShutdown {
private idService: IdService,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
) {
}
@bindThis
public async readNotification(
public async readAllNotification(
userId: User['id'],
notificationIds: Notification['id'][],
force = false,
) {
if (notificationIds.length === 0) return;
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
'-',
'COUNT', 1);
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
// Update documents
const result = await this.notificationsRepository.update({
notifieeId: userId,
id: In(notificationIds),
isRead: false,
}, {
isRead: true,
});
if (latestNotificationId == null) return;
if (result.affected === 0) return;
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
else return this.postReadNotifications(userId, notificationIds);
}
@bindThis
public async readNotificationByQuery(
userId: User['id'],
query: Record<string, any>,
) {
const notificationIds = await this.notificationsRepository.findBy({
...query,
notifieeId: userId,
isRead: false,
}).then(notifications => notifications.map(notification => notification.id));
return this.readNotification(userId, notificationIds);
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
return this.postReadAllNotifications(userId);
}
}
@bindThis
private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
}
@bindThis
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
}
@bindThis
@@ -90,45 +74,43 @@ export class NotificationService implements OnApplicationShutdown {
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
if (data.notifierId) {
if (notifieeId === data.notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(data.notifierId)) {
return null;
}
}
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
const notification = {
id: this.idService.genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data,
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
} as Notification;
const packed = await this.notificationEntityService.pack(notification, {});
const redisIdPromise = this.redisClient.xadd(
`notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300',
`${this.idService.parse(notification.id).date.getTime()}-*`,
'data', JSON.stringify(notification));
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) return; // 既に削除されているかもしれない
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId,
});
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
return;
}
//#endregion
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);

View File

@@ -15,10 +15,6 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAntenna': { antennaId: string };
'readAllAntennas': undefined;
};
// Reduce length because push message servers have character limits
@@ -72,14 +68,6 @@ export class PushNotificationService {
});
for (const subscription of subscriptions) {
// Continue if sendReadMessage is false
if ([
'readNotifications',
'readAllNotifications',
'readAntenna',
'readAllAntennas',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {

View File

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

View File

@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { KVCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable()
export class RelayService {
private relaysCache: KVCache<Relay[]>;
private relaysCache: MemorySingleCache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10);
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
}
@bindThis
@@ -109,7 +109,7 @@ export class RelayService {
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return;
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted',
}));
if (relays.length === 0) return;

View File

@@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
private rolesCache: KVCache<Role[]>;
private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>;
private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
@@ -77,15 +77,15 @@ export class RoleService implements OnApplicationShutdown {
private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService,
private userCacheService: UserCacheService,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new KVCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity);
this.rolesCache = new MemorySingleCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'roleCreated': {
const cached = this.rolesCache.get(null);
const cached = this.rolesCache.get();
if (cached) {
cached.push({
...body,
@@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
break;
}
case 'roleUpdated': {
const cached = this.rolesCache.get(null);
const cached = this.rolesCache.get();
if (cached) {
const i = cached.findIndex(x => x.id === body.id);
if (i > -1) {
@@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
break;
}
case 'roleDeleted': {
const cached = this.rolesCache.get(null);
const cached = this.rolesCache.get();
if (cached) {
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
this.rolesCache.set(cached.filter(x => x.id !== body.id));
}
break;
}
@@ -214,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles];
}
@@ -231,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else {
@@ -301,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)),
@@ -321,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public async getAdministratorIds(): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const administratorRoles = roles.filter(r => r.isAdministrator);
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(administratorRoles.map(r => r.id)),

View File

@@ -42,7 +42,7 @@ export class S3Service {
accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey,
} : undefined,
region: meta.objectStorageRegion ?? undefined,
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption),

View File

@@ -1,40 +1,30 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Redis from 'ioredis';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { IdService } from '@/core/IdService.js';
import type { User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
import Logger from '@/logger.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js';
import { KVCache } from '@/misc/cache.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@Injectable()
export class UserBlockingService implements OnApplicationShutdown {
export class UserBlockingService implements OnModuleInit {
private logger: Logger;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: KVCache<User['id'][]>;
private userFollowingService: UserFollowingService;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private moduleRef: ModuleRef,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
@@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'blockingCreated': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
}
break;
}
case 'blockingDeleted': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
}
break;
}
default:
break;
}
}
onModuleInit() {
this.userFollowingService = this.moduleRef.get('UserFollowingService');
}
@bindThis
@@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
await Promise.all([
this.cancelRequest(blocker, blockee),
this.cancelRequest(blockee, blocker),
this.unFollow(blocker, blockee),
this.unFollow(blockee, blocker),
this.userFollowingService.unfollow(blocker, blockee),
this.userFollowingService.unfollow(blockee, blocker),
this.removeFromList(blockee, blocker),
]);
@@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
await this.blockingsRepository.insert(blocking);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id,
blockeeId: blockee.id,
@@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@@ -173,54 +138,6 @@ export class UserBlockingService implements OnApplicationShutdown {
}
}
@bindThis
private async unFollow(follower: User, followee: User) {
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
});
if (following == null) {
return;
}
await Promise.all([
this.followingsRepository.delete(following.id),
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
this.perUserFollowingChart.update(follower, followee, false),
]);
// Publish unfollow event
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
});
}
// リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
}
// リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@bindThis
private async removeFromList(listOwner: User, user: User) {
const userLists = await this.userListsRepository.findBy({
@@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
await this.blockingsRepository.delete(blocking.id);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id,
blockeeId: blockee.id,
@@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
@bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
where: {
blockerId,
},
select: ['blockeeId'],
}).then(records => records.map(record => record.blockeeId)));
return blockedUserIds.includes(blockeeId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
}
}

View File

@@ -1,88 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserCacheService implements OnApplicationShutdown {
public userByIdCache: KVCache<User>;
public localUserByNativeTokenCache: KVCache<LocalUser | null>;
public localUserByIdCache: KVCache<LocalUser>;
public uriPersonCache: KVCache<User | null>;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new KVCache<User>(Infinity);
this.localUserByNativeTokenCache = new KVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new KVCache<LocalUser>(Infinity);
this.uriPersonCache = new KVCache<User | null>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
@@ -18,6 +19,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@@ -36,8 +38,12 @@ type Remote = RemoteUser | {
type Both = Local | Remote;
@Injectable()
export class UserFollowingService {
export class UserFollowingService implements OnModuleInit {
private userBlockingService: UserBlockingService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -53,8 +59,8 @@ export class UserFollowingService {
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
@@ -68,6 +74,10 @@ export class UserFollowingService {
) {
}
onModuleInit() {
this.userBlockingService = this.moduleRef.get('UserBlockingService');
}
@bindThis
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
const [follower, followee] = await Promise.all([
@@ -172,6 +182,8 @@ export class UserFollowingService {
}
});
this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id,
followerId: follower.id,
@@ -225,7 +237,6 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
@@ -279,6 +290,8 @@ export class UserFollowingService {
await this.followingsRepository.delete(following.id);
this.cacheService.userFollowingsCache.refresh(follower.id);
this.decrementFollowing(follower, followee);
// Publish unfollow event
@@ -286,7 +299,6 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@@ -579,7 +591,6 @@ export class UserFollowingService {
detail: true,
});
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));

View File

@@ -0,0 +1,34 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairService {
private cache: RedisKVCache<UserKeypair>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new RedisKVCache<UserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId);
}
}

View File

@@ -1,24 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairStoreService {
private cache: KVCache<UserKeypair>;
constructor(
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new KVCache<UserKeypair>(Infinity);
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId }));
}
}

View File

@@ -1,34 +1,47 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, MutingsRepository } from '@/models/index.js';
import { In } from 'typeorm';
import type { MutingsRepository, Muting } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable()
export class UserMutingService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private idService: IdService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private cacheService: CacheService,
) {
}
@bindThis
public async mute(user: User, target: User): Promise<void> {
public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
await this.mutingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: expiresAt ?? null,
muterId: user.id,
muteeId: target.id,
});
this.cacheService.userMutingsCache.refresh(user.id);
}
@bindThis
public async unmute(mutings: Muting[]): Promise<void> {
if (mutings.length === 0) return;
await this.mutingsRepository.delete({
id: In(mutings.map(m => m.id)),
});
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
this.cacheService.userMutingsCache.refresh(muterId);
}
}
}

View File

@@ -37,7 +37,7 @@ export class VideoProcessingService {
});
});
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280);
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
} finally {
cleanup();
}

View File

@@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
import { RemoteUser, User } from '@/models/entities/User.js';
@@ -31,8 +31,8 @@ export type UriParseResult = {
@Injectable()
export class ApDbResolverService {
private publicKeyCache: KVCache<UserPublickey | null>;
private publicKeyByUserIdCache: KVCache<UserPublickey | null>;
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
constructor(
@Inject(DI.config)
@@ -47,11 +47,11 @@ export class ApDbResolverService {
@Inject(DI.userPublickeysRepository)
private userPublickeysRepository: UserPublickeysRepository,
private userCacheService: UserCacheService,
private cacheService: CacheService,
private apPersonService: ApPersonService,
) {
this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity);
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
}
@bindThis
@@ -107,11 +107,11 @@ export class ApDbResolverService {
if (parsed.local) {
if (parsed.type !== 'users') return null;
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
} else {
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri,
}));
}
@@ -138,7 +138,7 @@ export class ApDbResolverService {
if (key == null) return null;
return {
user: await this.userCacheService.findById(key.userId) as RemoteUser,
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
key,
};
}

View File

@@ -14,13 +14,15 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { Poll } from '@/models/entities/Poll.js';
import type { PollVote } from '@/models/entities/PollVote.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
@@ -50,10 +52,11 @@ export class ApRendererService {
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService,
private userKeypairStoreService: UserKeypairStoreService,
private userKeypairService: UserKeypairService,
private apMfmService: ApMfmService,
private mfmService: MfmService,
) {
@@ -272,11 +275,7 @@ export class ApRendererService {
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
// TODO: cache
const emoji = await this.emojisRepository.findOneBy({
name,
host: IsNull(),
});
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
if (emoji) object.tag = [this.renderEmoji(emoji)];
}
@@ -473,7 +472,7 @@ export class ApRendererService {
...hashtagTags,
];
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const person = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
@@ -640,7 +639,7 @@ export class ApRendererService {
@bindThis
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false;
@@ -701,13 +700,9 @@ export class ApRendererService {
private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
const emojis = await Promise.all(
names.map(name => this.emojisRepository.findOneBy({
name,
host: IsNull(),
})),
);
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
return emojis.filter(emoji => emoji != null) as Emoji[];
return emojis;
}
}

View File

@@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
@@ -131,7 +131,7 @@ export class ApRequestService {
@Inject(DI.config)
private config: Config,
private userKeypairStoreService: UserKeypairStoreService,
private userKeypairService: UserKeypairService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@@ -143,7 +143,7 @@ export class ApRequestService {
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
const body = JSON.stringify(object);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedPost({
key: {
@@ -170,7 +170,7 @@ export class ApRequestService {
*/
@bindThis
public async signedGet(url: string, user: { id: User['id'] }) {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
key: {

View File

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

View File

@@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
import { truncate } from '@/misc/truncate.js';
import type { UserCacheService } from '@/core/UserCacheService.js';
import type { CacheService } from '@/core/CacheService.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type Logger from '@/logger.js';
@@ -54,7 +54,7 @@ export class ApPersonService implements OnModuleInit {
private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService;
private userCacheService: UserCacheService;
private cacheService: CacheService;
private apResolverService: ApResolverService;
private apNoteService: ApNoteService;
private apImageService: ApImageService;
@@ -97,7 +97,7 @@ export class ApPersonService implements OnModuleInit {
//private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private userCacheService: UserCacheService,
//private cacheService: CacheService,
//private apResolverService: ApResolverService,
//private apNoteService: ApNoteService,
//private apImageService: ApImageService,
@@ -118,7 +118,7 @@ export class ApPersonService implements OnModuleInit {
this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.userCacheService = this.moduleRef.get('UserCacheService');
this.cacheService = this.moduleRef.get('CacheService');
this.apResolverService = this.moduleRef.get('ApResolverService');
this.apNoteService = this.moduleRef.get('ApNoteService');
this.apImageService = this.moduleRef.get('ApImageService');
@@ -207,14 +207,14 @@ export class ApPersonService implements OnModuleInit {
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.userCacheService.uriPersonCache.get(uri);
const cached = this.cacheService.uriPersonCache.get(uri);
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(this.config.url + '/')) {
const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id });
if (u) this.userCacheService.uriPersonCache.set(uri, u);
if (u) this.cacheService.uriPersonCache.set(uri, u);
return u;
}
@@ -222,7 +222,7 @@ export class ApPersonService implements OnModuleInit {
const exist = await this.usersRepository.findOneBy({ uri });
if (exist) {
this.userCacheService.uriPersonCache.set(uri, exist);
this.cacheService.uriPersonCache.set(uri, exist);
return exist;
}
//#endregion

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { AntennasRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Antenna } from '@/models/entities/Antenna.js';
import { bindThis } from '@/decorators.js';
@@ -10,9 +10,6 @@ export class AntennaEntityService {
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
) {
}
@@ -22,8 +19,6 @@ export class AntennaEntityService {
): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
return {
id: antenna.id,
createdAt: antenna.createdAt.toISOString(),
@@ -38,7 +33,7 @@ export class AntennaEntityService {
withReplies: antenna.withReplies,
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote,
hasUnreadNote: false, // TODO
};
}
}

View File

@@ -1,13 +1,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';
import { In } from 'typeorm';
@Injectable()
export class ChannelEntityService {
@@ -18,13 +19,19 @@ export class ChannelEntityService {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
@@ -33,6 +40,7 @@ export class ChannelEntityService {
public async pack(
src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined,
detailed?: boolean,
): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null;
@@ -46,6 +54,17 @@ export class ChannelEntityService {
followeeId: channel.id,
}) : null;
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
userId: meId,
channelId: channel.id,
}) : null;
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
where: {
id: In(channel.pinnedNoteIds),
},
}) : [];
return {
id: channel.id,
createdAt: channel.createdAt.toISOString(),
@@ -54,13 +73,19 @@ export class ChannelEntityService {
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
pinnedNoteIds: channel.pinnedNoteIds,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
...(me ? {
isFollowing: following != null,
isFavorited: favorite != null,
hasUnreadNote,
} : {}),
...(detailed ? {
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
} : {}),
};
}
}

View File

@@ -406,7 +406,7 @@ export class NoteEntityService implements OnModuleInit {
}
}
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
@@ -420,6 +420,30 @@ export class NoteEntityService implements OnModuleInit {
})));
}
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
@bindThis
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える

View File

@@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
import type { AccessTokensRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js';
import type { Note } from '@/models/entities/Note.js';
@@ -25,8 +26,11 @@ export class NotificationEntityService implements OnModuleInit {
constructor(
private moduleRef: ModuleRef,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@@ -48,30 +52,40 @@ export class NotificationEntityService implements OnModuleInit {
@bindThis
public async pack(
src: Notification['id'] | Notification,
src: Notification,
meId: User['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
_hint_?: {
packedNotes: Map<Note['id'], Packed<'Note'>>;
};
},
hint?: {
packedNotes: Map<Note['id'], Packed<'Note'>>;
packedUsers: Map<User['id'], Packed<'User'>>;
},
): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src });
const notification = src;
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
options._hint_?.packedNotes != null
? options._hint_.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.noteId!, { id: meId }, {
detail: true,
})
) : undefined;
const userIfNeed = notification.notifierId != null ? (
hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId!, { id: meId }, {
detail: false,
})
) : undefined;
return await awaitAll({
id: notification.id,
createdAt: notification.createdAt.toISOString(),
createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
isRead: notification.isRead,
userId: notification.notifierId,
user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
...(userIfNeed != null ? { user: userIfNeed } : {}),
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
@@ -87,33 +101,36 @@ export class NotificationEntityService implements OnModuleInit {
});
}
/**
* @param notifications you should join "note" property when fetch from DB, and all notifieeId should be same as meId
*/
@bindThis
public async packMany(
notifications: Notification[],
meId: User['id'],
) {
if (notifications.length === 0) return [];
for (const notification of notifications) {
if (meId !== notification.notifieeId) {
// because we call note packMany with meId, all notifieeId should be same as meId
throw new Error('TRY_TO_PACK_ANOTHER_USER_NOTIFICATION');
}
}
const notes = notifications.map(x => x.note).filter(isNotNull);
const noteIds = notifications.map(x => x.noteId).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'user.avatar', 'user.banner', 'reply', 'reply.user', 'reply.user.avatar', 'reply.user.banner', 'renote', 'renote.user', 'renote.user.avatar', 'renote.user.banner'],
}) : [];
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
return await Promise.all(notifications.map(x => this.pack(x, {
_hint_: {
packedNotes,
},
const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
relations: ['avatar', 'banner'],
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
detail: false,
});
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
return await Promise.all(notifications.map(x => this.pack(x, meId, {}, {
packedNotes,
packedUsers,
})));
}
}

View File

@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import Redis from 'ioredis';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
@@ -8,11 +9,11 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -52,7 +53,7 @@ export class UserEntityService implements OnModuleInit {
private customEmojiService: CustomEmojiService;
private antennaService: AntennaService;
private roleService: RoleService;
private userInstanceCache: KVCache<Instance | null>;
private userInstanceCache: MemoryKVCache<Instance | null>;
constructor(
private moduleRef: ModuleRef,
@@ -60,6 +61,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.config)
private config: Config,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -90,9 +94,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
@@ -108,9 +109,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@@ -121,7 +119,7 @@ export class UserEntityService implements OnModuleInit {
//private antennaService: AntennaService,
//private roleService: RoleService,
) {
this.userInstanceCache = new KVCache<Instance | null>(1000 * 60 * 60 * 3);
this.userInstanceCache = new MemoryKVCache<Instance | null>(1000 * 60 * 60 * 3);
}
onModuleInit() {
@@ -223,6 +221,7 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
@@ -231,37 +230,22 @@ export class UserEntityService implements OnModuleInit {
}) : null;
return unread != null;
}
@bindThis
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
const channels = await this.channelFollowingsRepository.findBy({ followerId: userId });
const unread = channels.length > 0 ? await this.noteUnreadsRepository.findOneBy({
userId: userId,
noteChannelId: In(channels.map(x => x.followeeId)),
}) : null;
return unread != null;
*/
return false; // TODO
}
@bindThis
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
const mutedUserIds = mute.map(m => m.muteeId);
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
'-',
'COUNT', 1);
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
const count = await this.notificationsRepository.count({
where: {
notifieeId: userId,
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
isRead: false,
},
take: 1,
});
return count > 0;
return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
}
@bindThis
@@ -467,7 +451,7 @@ export class UserEntityService implements OnModuleInit {
}).then(count => count > 0),
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: this.getHasUnreadChannel(user.id),
hasUnreadChannel: false, // 後方互換性のため
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords,

View File

@@ -33,7 +33,6 @@ export const DI = {
emojisRepository: Symbol('emojisRepository'),
driveFilesRepository: Symbol('driveFilesRepository'),
driveFoldersRepository: Symbol('driveFoldersRepository'),
notificationsRepository: Symbol('notificationsRepository'),
metasRepository: Symbol('metasRepository'),
mutingsRepository: Symbol('mutingsRepository'),
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
@@ -54,14 +53,13 @@ export const DI = {
clipNotesRepository: Symbol('clipNotesRepository'),
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
antennasRepository: Symbol('antennasRepository'),
antennaNotesRepository: Symbol('antennaNotesRepository'),
promoNotesRepository: Symbol('promoNotesRepository'),
promoReadsRepository: Symbol('promoReadsRepository'),
relaysRepository: Symbol('relaysRepository'),
mutedNotesRepository: Symbol('mutedNotesRepository'),
channelsRepository: Symbol('channelsRepository'),
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
channelNotePiningsRepository: Symbol('channelNotePiningsRepository'),
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
registryItemsRepository: Symbol('registryItemsRepository'),
webhooksRepository: Symbol('webhooksRepository'),
adsRepository: Symbol('adsRepository'),

View File

@@ -1,18 +1,187 @@
import 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;
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;
this.lifetime = opts.lifetime;
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
this.toRedisConverter = opts.toRedisConverter;
this.fromRedisConverter = opts.fromRedisConverter;
}
@bindThis
public async set(key: string, value: T): Promise<void> {
this.memoryCache.set(key, value);
if (this.lifetime === Infinity) {
await this.redisClient.set(
`kvcache:${this.name}:${key}`,
this.toRedisConverter(value),
);
} else {
await this.redisClient.set(
`kvcache:${this.name}:${key}`,
this.toRedisConverter(value),
'ex', Math.round(this.lifetime / 1000),
);
}
}
@bindThis
public async get(key: string): Promise<T | undefined> {
const memoryCached = this.memoryCache.get(key);
if (memoryCached !== undefined) return memoryCached;
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
}
@bindThis
public async delete(key: string): Promise<void> {
this.memoryCache.delete(key);
await this.redisClient.del(`kvcache:${this.name}:${key}`);
}
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
*/
@bindThis
public async fetch(key: string): Promise<T> {
const cachedValue = await this.get(key);
if (cachedValue !== undefined) {
// Cache HIT
return cachedValue;
}
// Cache MISS
const value = await this.fetcher(key);
this.set(key, value);
return value;
}
@bindThis
public async refresh(key: string) {
const value = await this.fetcher(key);
this.set(key, value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
}
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;
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
lifetime: RedisSingleCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisSingleCache<T>['fetcher'];
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
}) {
this.redisClient = redisClient;
this.name = name;
this.lifetime = opts.lifetime;
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
this.toRedisConverter = opts.toRedisConverter;
this.fromRedisConverter = opts.fromRedisConverter;
}
@bindThis
public async set(value: T): Promise<void> {
this.memoryCache.set(value);
if (this.lifetime === Infinity) {
await this.redisClient.set(
`singlecache:${this.name}`,
this.toRedisConverter(value),
);
} else {
await this.redisClient.set(
`singlecache:${this.name}`,
this.toRedisConverter(value),
'ex', Math.round(this.lifetime / 1000),
);
}
}
@bindThis
public async get(): Promise<T | undefined> {
const memoryCached = this.memoryCache.get();
if (memoryCached !== undefined) return memoryCached;
const cached = await this.redisClient.get(`singlecache:${this.name}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
}
@bindThis
public async delete(): Promise<void> {
this.memoryCache.delete();
await this.redisClient.del(`singlecache:${this.name}`);
}
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
*/
@bindThis
public async fetch(): Promise<T> {
const cachedValue = await this.get();
if (cachedValue !== undefined) {
// Cache HIT
return cachedValue;
}
// Cache MISS
const value = await this.fetcher();
this.set(value);
return value;
}
@bindThis
public async refresh() {
const value = await this.fetcher();
this.set(value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
}
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class KVCache<T> {
public cache: Map<string | null, { date: number; value: T; }>;
export class MemoryKVCache<T> {
public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
constructor(lifetime: KVCache<never>['lifetime']) {
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
}
@bindThis
public set(key: string | null, value: T): void {
public set(key: string, value: T): void {
this.cache.set(key, {
date: Date.now(),
value,
@@ -20,7 +189,7 @@ export class KVCache<T> {
}
@bindThis
public get(key: string | null): T | undefined {
public get(key: string): T | undefined {
const cached = this.cache.get(key);
if (cached == null) return undefined;
if ((Date.now() - cached.date) > this.lifetime) {
@@ -31,7 +200,7 @@ export class KVCache<T> {
}
@bindThis
public delete(key: string | null) {
public delete(key: string) {
this.cache.delete(key);
}
@@ -40,7 +209,7 @@ export class KVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
@@ -65,7 +234,7 @@ export class KVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
@@ -88,12 +257,12 @@ export class KVCache<T> {
}
}
export class Cache<T> {
export class MemorySingleCache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
this.lifetime = lifetime;
}

View File

@@ -23,3 +23,8 @@ export function genAid(date: Date): string {
counter++;
return getTime(t) + getNoise();
}
export function parseAid(id: string): { date: Date; } {
const time = parseInt(id.slice(0, 8), 36) + TIME2000;
return { date: new Date(time) };
}

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -172,12 +172,6 @@ const $driveFoldersRepository: Provider = {
inject: [DI.db],
};
const $notificationsRepository: Provider = {
provide: DI.notificationsRepository,
useFactory: (db: DataSource) => db.getRepository(Notification),
inject: [DI.db],
};
const $metasRepository: Provider = {
provide: DI.metasRepository,
useFactory: (db: DataSource) => db.getRepository(Meta),
@@ -298,12 +292,6 @@ const $antennasRepository: Provider = {
inject: [DI.db],
};
const $antennaNotesRepository: Provider = {
provide: DI.antennaNotesRepository,
useFactory: (db: DataSource) => db.getRepository(AntennaNote),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
useFactory: (db: DataSource) => db.getRepository(PromoNote),
@@ -340,9 +328,9 @@ const $channelFollowingsRepository: Provider = {
inject: [DI.db],
};
const $channelNotePiningsRepository: Provider = {
provide: DI.channelNotePiningsRepository,
useFactory: (db: DataSource) => db.getRepository(ChannelNotePining),
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(ChannelFavorite),
inject: [DI.db],
};
@@ -432,7 +420,6 @@ const $roleAssignmentsRepository: Provider = {
$emojisRepository,
$driveFilesRepository,
$driveFoldersRepository,
$notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
@@ -453,14 +440,13 @@ const $roleAssignmentsRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
$mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelNotePiningsRepository,
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
$adsRepository,
@@ -500,7 +486,6 @@ const $roleAssignmentsRepository: Provider = {
$emojisRepository,
$driveFilesRepository,
$driveFoldersRepository,
$notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
@@ -521,14 +506,13 @@ const $roleAssignmentsRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaNotesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
$mutedNotesRepository,
$channelsRepository,
$channelFollowingsRepository,
$channelNotePiningsRepository,
$channelFavoritesRepository,
$registryItemsRepository,
$webhooksRepository,
$adsRepository,

View File

@@ -1,43 +0,0 @@
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
import { id } from '../id.js';
import { Note } from './Note.js';
import { Antenna } from './Antenna.js';
@Entity()
@Index(['noteId', 'antennaId'], { unique: true })
export class AntennaNote {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
comment: 'The note ID.',
})
public noteId: Note['id'];
@ManyToOne(type => Note, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: Note | null;
@Index()
@Column({
...id(),
comment: 'The antenna ID.',
})
public antennaId: Antenna['id'];
@ManyToOne(type => Antenna, {
onDelete: 'CASCADE',
})
@JoinColumn()
public antenna: Antenna | null;
@Index()
@Column('boolean', {
default: false,
})
public read: boolean;
}

View File

@@ -59,6 +59,11 @@ export class Channel {
@JoinColumn()
public banner: DriveFile | null;
@Column('varchar', {
array: true, length: 128, default: '{}',
})
public pinnedNoteIds: string[];
@Index()
@Column('integer', {
default: 0,

View File

@@ -1,21 +1,24 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { Note } from './Note.js';
import { User } from './User.js';
import { Channel } from './Channel.js';
@Entity()
@Index(['channelId', 'noteId'], { unique: true })
export class ChannelNotePining {
@Index(['userId', 'channelId'], { unique: true })
export class ChannelFavorite {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the ChannelNotePining.',
comment: 'The created date of the ChannelFavorite.',
})
public createdAt: Date;
@Index()
@Column(id())
@Column({
...id(),
})
public channelId: Channel['id'];
@ManyToOne(type => Channel, {
@@ -24,12 +27,15 @@ export class ChannelNotePining {
@JoinColumn()
public channel: Channel | null;
@Column(id())
public noteId: Note['id'];
@Index()
@Column({
...id(),
})
public userId: User['id'];
@ManyToOne(type => Note, {
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: Note | null;
public user: User | null;
}

View File

@@ -1,54 +1,19 @@
import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
import { notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { id } from '../id.js';
import { notificationTypes } from '@/types.js';
import { User } from './User.js';
import { Note } from './Note.js';
import { FollowRequest } from './FollowRequest.js';
import { AccessToken } from './AccessToken.js';
@Entity()
export class Notification {
@PrimaryColumn(id())
public id: string;
export type Notification = {
id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Notification.',
})
public createdAt: Date;
/**
* 通知の受信者
*/
@Index()
@Column({
...id(),
comment: 'The ID of recipient user of the Notification.',
})
public notifieeId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public notifiee: User | null;
// RedisのためDateではなくstring
createdAt: string;
/**
* 通知の送信者(initiator)
*/
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of sender user of the Notification.',
})
public notifierId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public notifier: User | null;
notifierId: User['id'] | null;
/**
* 通知の種類。
@@ -64,104 +29,37 @@ export class Notification {
* achievementEarned - 実績を獲得
* app - アプリ通知
*/
@Index()
@Column('enum', {
enum: [
...notificationTypes,
...obsoleteNotificationTypes,
],
comment: 'The type of the Notification.',
})
public type: typeof notificationTypes[number];
type: typeof notificationTypes[number];
/**
* 通知が読まれたかどうか
*/
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the Notification is read.',
})
public isRead: boolean;
noteId: Note['id'] | null;
@Column({
...id(),
nullable: true,
})
public noteId: Note['id'] | null;
followRequestId: FollowRequest['id'] | null;
@ManyToOne(type => Note, {
onDelete: 'CASCADE',
})
@JoinColumn()
public note: Note | null;
reaction: string | null;
@Column({
...id(),
nullable: true,
})
public followRequestId: FollowRequest['id'] | null;
choice: number | null;
@ManyToOne(type => FollowRequest, {
onDelete: 'CASCADE',
})
@JoinColumn()
public followRequest: FollowRequest | null;
@Column('varchar', {
length: 128, nullable: true,
})
public reaction: string | null;
@Column('integer', {
nullable: true,
})
public choice: number | null;
@Column('varchar', {
length: 128, nullable: true,
})
public achievement: string | null;
achievement: string | null;
/**
* アプリ通知のbody
*/
@Column('varchar', {
length: 2048, nullable: true,
})
public customBody: string | null;
customBody: string | null;
/**
* アプリ通知のheader
* (省略時はアプリ名で表示されることを期待)
*/
@Column('varchar', {
length: 256, nullable: true,
})
public customHeader: string | null;
customHeader: string | null;
/**
* アプリ通知のicon(URL)
* (省略時はアプリアイコンで表示されることを期待)
*/
@Column('varchar', {
length: 1024, nullable: true,
})
public customIcon: string | null;
customIcon: string | null;
/**
* アプリ通知のアプリ(のトークン)
*/
@Index()
@Column({
...id(),
nullable: true,
})
public appAccessTokenId: AccessToken['id'] | null;
@ManyToOne(type => AccessToken, {
onDelete: 'CASCADE',
})
@JoinColumn()
public appAccessToken: AccessToken | null;
appAccessTokenId: AccessToken['id'] | null;
}

View File

@@ -4,13 +4,12 @@ import { Ad } from '@/models/entities/Ad.js';
import { Announcement } from '@/models/entities/Announcement.js';
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { Antenna } from '@/models/entities/Antenna.js';
import { AntennaNote } from '@/models/entities/AntennaNote.js';
import { App } from '@/models/entities/App.js';
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { AuthSession } from '@/models/entities/AuthSession.js';
import { Blocking } from '@/models/entities/Blocking.js';
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
import { Clip } from '@/models/entities/Clip.js';
import { ClipNote } from '@/models/entities/ClipNote.js';
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
@@ -33,7 +32,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
import { NoteUnread } from '@/models/entities/NoteUnread.js';
import { Notification } from '@/models/entities/Notification.js';
import { Page } from '@/models/entities/Page.js';
import { PageLike } from '@/models/entities/PageLike.js';
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
@@ -73,13 +71,12 @@ export {
Announcement,
AnnouncementRead,
Antenna,
AntennaNote,
App,
AttestationChallenge,
AuthSession,
Blocking,
ChannelFollowing,
ChannelNotePining,
ChannelFavorite,
Clip,
ClipNote,
ClipFavorite,
@@ -102,7 +99,6 @@ export {
NoteReaction,
NoteThreadMuting,
NoteUnread,
Notification,
Page,
PageLike,
PasswordResetRequest,
@@ -141,13 +137,12 @@ export type AdsRepository = Repository<Ad>;
export type AnnouncementsRepository = Repository<Announcement>;
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
export type AntennasRepository = Repository<Antenna>;
export type AntennaNotesRepository = Repository<AntennaNote>;
export type AppsRepository = Repository<App>;
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
export type AuthSessionsRepository = Repository<AuthSession>;
export type BlockingsRepository = Repository<Blocking>;
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
export type ChannelFavoritesRepository = Repository<ChannelFavorite>;
export type ClipsRepository = Repository<Clip>;
export type ClipNotesRepository = Repository<ClipNote>;
export type ClipFavoritesRepository = Repository<ClipFavorite>;
@@ -170,7 +165,6 @@ export type NoteFavoritesRepository = Repository<NoteFavorite>;
export type NoteReactionsRepository = Repository<NoteReaction>;
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
export type NoteUnreadsRepository = Repository<NoteUnread>;
export type NotificationsRepository = Repository<Notification>;
export type PagesRepository = Repository<Page>;
export type PageLikesRepository = Repository<PageLike>;
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;

View File

@@ -42,10 +42,22 @@ export const packedChannelSchema = {
type: 'boolean',
optional: true, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
userId: {
type: 'string',
nullable: true, optional: false,
format: 'id',
},
pinnedNoteIds: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
format: 'id',
},
},
},
} as const;

View File

@@ -14,10 +14,6 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
format: 'date-time',
},
isRead: {
type: 'boolean',
optional: false, nullable: false,
},
type: {
type: 'string',
optional: false, nullable: false,

View File

@@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadChannel: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadNotification: {
type: 'boolean',
nullable: false, optional: false,

View File

@@ -12,13 +12,12 @@ import { Ad } from '@/models/entities/Ad.js';
import { Announcement } from '@/models/entities/Announcement.js';
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
import { Antenna } from '@/models/entities/Antenna.js';
import { AntennaNote } from '@/models/entities/AntennaNote.js';
import { App } from '@/models/entities/App.js';
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
import { AuthSession } from '@/models/entities/AuthSession.js';
import { Blocking } from '@/models/entities/Blocking.js';
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
import { Clip } from '@/models/entities/Clip.js';
import { ClipNote } from '@/models/entities/ClipNote.js';
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
@@ -41,7 +40,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
import { NoteUnread } from '@/models/entities/NoteUnread.js';
import { Notification } from '@/models/entities/Notification.js';
import { Page } from '@/models/entities/Page.js';
import { PageLike } from '@/models/entities/PageLike.js';
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
@@ -156,7 +154,6 @@ export const entities = [
DriveFolder,
Poll,
PollVote,
Notification,
Emoji,
Hashtag,
SwSubscription,
@@ -168,14 +165,13 @@ export const entities = [
ClipNote,
ClipFavorite,
Antenna,
AntennaNote,
PromoNote,
PromoRead,
Relay,
MutedNote,
Channel,
ChannelFollowing,
ChannelNotePining,
ChannelFavorite,
RegistryItem,
Ad,
PasswordResetRequest,

View File

@@ -4,10 +4,10 @@ import { DI } from '@/di-symbols.js';
import type { MutingsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable()
export class CheckExpiredMutingsProcessorService {
@@ -20,7 +20,7 @@ export class CheckExpiredMutingsProcessorService {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private globalEventService: GlobalEventService,
private userMutingService: UserMutingService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
@@ -37,13 +37,7 @@ export class CheckExpiredMutingsProcessorService {
.getMany();
if (expired.length > 0) {
await this.mutingsRepository.delete({
id: In(expired.map(m => m.id)),
});
for (const m of expired) {
this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!);
}
await this.userMutingService.unmute(expired);
}
this.logger.succ('All expired mutings checked.');

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
@@ -20,18 +20,12 @@ export class CleanProcessorService {
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutedNotesRepository)
private mutedNotesRepository: MutedNotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
@@ -49,10 +43,6 @@ export class CleanProcessorService {
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
});
this.notificationsRepository.delete({
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
});
this.mutedNotesRepository.delete({
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
reason: 'word',

View File

@@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { KVCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import ApRequestChart from '@/core/chart/charts/ap-request.js';
@@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
@Injectable()
export class DeliverProcessorService {
private logger: Logger;
private suspendedHostsCache: KVCache<Instance[]>;
private suspendedHostsCache: MemorySingleCache<Instance[]>;
private latest: string | null;
constructor(
@@ -46,7 +46,7 @@ export class DeliverProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new KVCache<Instance[]>(1000 * 60 * 60);
this.suspendedHostsCache = new MemorySingleCache<Instance[]>(1000 * 60 * 60);
}
@bindThis
@@ -60,14 +60,14 @@ export class DeliverProcessorService {
}
// isSuspendedなら中断
let suspendedHosts = this.suspendedHostsCache.get(null);
let suspendedHosts = this.suspendedHostsCache.get();
if (suspendedHosts == null) {
suspendedHosts = await this.instancesRepository.find({
where: {
isSuspended: true,
},
});
this.suspendedHostsCache.set(null, suspendedHosts);
this.suspendedHostsCache.set(suspendedHosts);
}
if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) {
return 'skip (suspended)';

View File

@@ -86,6 +86,10 @@ export class ImportCustomEmojisProcessorService {
continue;
}
const emojiInfo = record.emoji;
if (!/^[a-zA-Z0-9_]+$/.test(emojiInfo.name)) {
this.logger.error(`invalid emojiname: ${emojiInfo.name}`);
continue;
}
const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({
name: emojiInfo.name,

View File

@@ -39,6 +39,7 @@ export class WebhookDeliverProcessorService {
'X-Misskey-Host': this.config.host,
'X-Misskey-Hook-Id': job.data.webhookId,
'X-Misskey-Hook-Secret': job.data.secret,
'Content-Type': 'application/json',
},
body: JSON.stringify({
hookId: job.data.webhookId,

View File

@@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { QueueService } from '@/core/QueueService.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import type { Following } from '@/models/entities/Following.js';
import { countIf } from '@/misc/prelude/array.js';
import type { Note } from '@/models/entities/Note.js';
@@ -58,7 +58,7 @@ export class ActivityPubServerService {
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private queueService: QueueService,
private userKeypairStoreService: UserKeypairStoreService,
private userKeypairService: UserKeypairService,
private queryService: QueryService,
) {
//this.createServer = this.createServer.bind(this);
@@ -540,7 +540,7 @@ export class ActivityPubServerService {
return;
}
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
const keypair = await this.userKeypairService.getUserKeypair(user.id);
if (this.userEntityService.isLocalUser(user)) {
reply.header('Cache-Control', 'public, max-age=180');

View File

@@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { KVCache } from '@/misc/cache.js';
import { MemorySingleCache } from '@/misc/cache.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js';
@@ -118,17 +118,17 @@ export class NodeinfoServerService {
};
};
const cache = new KVCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(null, () => nodeinfo2());
const base = await cache.fetch(() => nodeinfo2());
reply.header('Cache-Control', 'public, max-age=600');
return { version: '2.1', ...base };
});
fastify.get(nodeinfo2_0path, async (request, reply) => {
const base = await cache.fetch(null, () => nodeinfo2());
const base = await cache.fetch(() => nodeinfo2());
delete (base as any).software.repository;

View File

@@ -3,9 +3,9 @@ import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
import type { LocalUser } from '@/models/entities/User.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import { KVCache } from '@/misc/cache.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { App } from '@/models/entities/App.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import { CacheService } from '@/core/CacheService.js';
import isNativeToken from '@/misc/is-native-token.js';
import { bindThis } from '@/decorators.js';
@@ -18,7 +18,7 @@ export class AuthenticationError extends Error {
@Injectable()
export class AuthenticateService {
private appCache: KVCache<App>;
private appCache: MemoryKVCache<App>;
constructor(
@Inject(DI.usersRepository)
@@ -30,9 +30,9 @@ export class AuthenticateService {
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
private userCacheService: UserCacheService,
private cacheService: CacheService,
) {
this.appCache = new KVCache<App>(Infinity);
this.appCache = new MemoryKVCache<App>(Infinity);
}
@bindThis
@@ -42,7 +42,7 @@ export class AuthenticateService {
}
if (isNativeToken(token)) {
const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token,
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
if (user == null) {
@@ -67,7 +67,7 @@ export class AuthenticateService {
lastUsedAt: new Date(),
});
const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId,
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
() => this.usersRepository.findOneBy({
id: accessToken.userId,
}) as Promise<LocalUser>);

View File

@@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
import * as ep___channels_update from './endpoints/channels/update.js';
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
@@ -265,7 +268,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_read from './endpoints/notifications/read.js';
import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js';
@@ -424,6 +426,9 @@ const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___c
const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
@@ -594,7 +599,6 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
const $notifications_read: Provider = { provide: 'ep:notifications/read', useClass: ep___notifications_read.default };
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
@@ -757,6 +761,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$channels_timeline,
$channels_unfollow,
$channels_update,
$channels_favorite,
$channels_unfavorite,
$channels_myFavorites,
$charts_activeUsers,
$charts_apRequest,
$charts_drive,
@@ -927,7 +934,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_userListTimeline,
$notifications_create,
$notifications_markAllAsRead,
$notifications_read,
$pagePush,
$pages_create,
$pages_delete,
@@ -1084,6 +1090,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$channels_timeline,
$channels_unfollow,
$channels_update,
$channels_favorite,
$channels_unfavorite,
$channels_myFavorites,
$charts_activeUsers,
$charts_apRequest,
$charts_drive,
@@ -1254,7 +1263,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_userListTimeline,
$notifications_create,
$notifications_markAllAsRead,
$notifications_read,
$pagePush,
$pages_create,
$pages_delete,

View File

@@ -9,6 +9,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { AuthenticateService } from './AuthenticateService.js';
import MainStreamConnection from './stream/index.js';
import { ChannelsService } from './stream/ChannelsService.js';
@@ -45,7 +46,7 @@ export class StreamingApiServerService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private globalEventService: GlobalEventService,
private cacheService: CacheService,
private noteReadService: NoteReadService,
private authenticateService: AuthenticateService,
private channelsService: ChannelsService,
@@ -73,8 +74,6 @@ export class StreamingApiServerService {
return;
}
const connection = request.accept();
const ev = new EventEmitter();
async function onRedisMessage(_: string, data: string): Promise<void> {
@@ -85,19 +84,19 @@ export class StreamingApiServerService {
this.redisSubscriber.on('message', onRedisMessage);
const main = new MainStreamConnection(
this.followingsRepository,
this.mutingsRepository,
this.renoteMutingsRepository,
this.blockingsRepository,
this.channelFollowingsRepository,
this.userProfilesRepository,
this.channelsService,
this.globalEventService,
this.noteReadService,
this.notificationService,
connection, ev, user, miapp,
this.cacheService,
ev, user, miapp,
);
await main.init();
const connection = request.accept();
main.init2(connection);
const intervalId = user ? setInterval(() => {
this.usersRepository.update(user.id, {
lastActiveDate: new Date(),

View File

@@ -95,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
import * as ep___channels_update from './endpoints/channels/update.js';
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
@@ -265,7 +268,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_read from './endpoints/notifications/read.js';
import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js';
@@ -422,6 +424,9 @@ const eps = [
['channels/timeline', ep___channels_timeline],
['channels/unfollow', ep___channels_unfollow],
['channels/update', ep___channels_update],
['channels/favorite', ep___channels_favorite],
['channels/unfavorite', ep___channels_unfavorite],
['channels/my-favorites', ep___channels_myFavorites],
['charts/active-users', ep___charts_activeUsers],
['charts/ap-request', ep___charts_apRequest],
['charts/drive', ep___charts_drive],
@@ -592,7 +597,6 @@ const eps = [
['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
['notifications/read', ep___notifications_read],
['page-push', ep___pagePush],
['pages/create', ep___pages_create],
['pages/delete', ep___pages_delete],

View File

@@ -61,11 +61,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.usersRepository.update(user.id, {
isDeleted: true,
});
if (this.userEntityService.isLocalUser(user)) {
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
});
}
}

View File

@@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@@ -26,38 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
});
}
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
});
}
}

View File

@@ -90,8 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
license: emoji.license,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(copied.id),
});

View File

@@ -1,11 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService';
export const meta = {
tags: ['admin'],
@@ -24,38 +19,14 @@ export const paramDef = {
required: ['ids'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
await this.db.queryResultCache?.remove(['meta_emojis']);
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
}
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
await this.customEmojiService.deleteBulk(ps.ids);
});
}
}

View File

@@ -1,12 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApiError } from '../../../error.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@@ -31,38 +25,14 @@ export const paramDef = {
required: ['id'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await this.emojisRepository.delete(emoji.id);
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
await this.customEmojiService.delete(ps.id);
});
}
}

View File

@@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@@ -26,38 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
});
}
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
});
}
}

View File

@@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@@ -26,34 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
aliases: ps.aliases,
});
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
});
}
}

View File

@@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@@ -28,34 +24,14 @@ export const paramDef = {
required: ['ids'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
category: ps.category,
});
await this.db.queryResultCache?.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
});
}
}

View File

@@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -45,51 +41,19 @@ export const paramDef = {
required: ['id', 'name', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: ps.name, host: IsNull() });
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
if (sameNameEmoji != null && sameNameEmoji.id !== ps.id) throw new ApiError(meta.errors.sameNameEmojiExists);
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
await this.customEmojiService.update(ps.id, {
name: ps.name,
category: ps.category,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license,
license: ps.license ?? null,
});
await this.db.queryResultCache?.remove(['meta_emojis']);
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === ps.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
});
}
}

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js';
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -36,9 +36,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
private userEntityService: UserEntityService,
private userFollowingService: UserFollowingService,
private userSuspendService: UserSuspendService,
@@ -65,15 +62,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
targetId: user.id,
});
// Terminate streaming
if (this.userEntityService.isLocalUser(user)) {
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
(async () => {
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
await this.readAllNotify(user).catch(e => {});
})();
});
}
@@ -96,14 +87,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userFollowingService.unfollow(follower, followee, true);
}
}
@bindThis
private async readAllNotify(notifier: User) {
await this.notificationsRepository.update({
notifierId: notifier.id,
isRead: false,
}, {
isRead: true,
});
}
}

View File

@@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -50,15 +52,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private noteReadService: NoteReadService,
@@ -73,9 +76,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchAntenna);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
const noteIdsRes = await this.redisClient.xrevrange(
`antennaTimeline:${antenna.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
if (noteIdsRes.length === 0) {
return [];
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];
}
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -86,16 +104,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
const notes = await query
.take(ps.limit)
.getMany();
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
if (notes.length > 0) {
this.noteReadService.read(me.id, notes);

View File

@@ -0,0 +1,61 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
requireCredential: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '4938f5f3-6167-4c04-9149-6607b7542861',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await this.channelFavoritesRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
channelId: channel.id,
});
});
}
}

View File

@@ -41,7 +41,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private channelFollowingsRepository: ChannelFollowingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
@@ -58,8 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
followerId: me.id,
followeeId: channel.id,
});
this.globalEventService.publishUserEvent(me.id, 'followChannel', channel);
});
}
}

View File

@@ -0,0 +1,54 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['channels', 'account'],
requireCredential: true,
kind: 'read:channels',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Channel',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
private channelEntityService: ChannelEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.channelFavoritesRepository.createQueryBuilder('favorite')
.andWhere('favorite.userId = :meId', { meId: me.id })
.leftJoinAndSelect('favorite.channel', 'channel');
const favorites = await query
.getMany();
return await Promise.all(favorites.map(x => this.channelEntityService.pack(x.channel!, me)));
});
}
}

View File

@@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchChannel);
}
return await this.channelEntityService.pack(channel, me);
return await this.channelEntityService.pack(channel, me, true);
});
}
}

View File

@@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -48,12 +50,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private activeUsersChart: ActiveUsersChart,
@@ -67,9 +73,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchChannel);
}
const noteIdsRes = await this.redisClient.xrevrange(
`channelTimeline:${channel.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
if (noteIdsRes.length === 0) {
return [];
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];
}
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId = :channelId', { channelId: channel.id })
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -90,7 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
const timeline = await query.getMany();
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
if (me) this.activeUsersChart.read(me);

View File

@@ -0,0 +1,56 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
requireCredential: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '353c68dd-131a-476c-aa99-88a345e83668',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await this.channelFavoritesRepository.delete({
userId: me.id,
channelId: channel.id,
});
});
}
}

View File

@@ -38,8 +38,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
@@ -54,8 +52,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
followerId: me.id,
followeeId: channel.id,
});
this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel);
});
}
}

View File

@@ -3,8 +3,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
@@ -47,6 +47,12 @@ export const paramDef = {
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
pinnedNoteIds: {
type: 'array',
items: {
type: 'string', format: 'misskey:id',
},
},
},
required: ['channelId'],
} as const;
@@ -64,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private channelEntityService: ChannelEntityService,
private roleService: RoleService,
) {
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
@@ -97,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.channelsRepository.update(channel.id, {
...(ps.name !== undefined ? { name: ps.name } : {}),
...(ps.description !== undefined ? { description: ps.description } : {}),
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
...(banner ? { bannerId: banner.id } : {}),
});

View File

@@ -58,10 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
category: 'ASC',
name: 'ASC',
},
cache: {
id: 'meta_emojis',
milliseconds: 3600000, // 1 hour
},
});
return {

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