Compare commits
20 Commits
13.11.0.be
...
misskey-js
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0b0a416566 | ||
![]() |
9044fa5d1a | ||
![]() |
3bb343e2fc | ||
![]() |
f2fd8bfac1 | ||
![]() |
1602ad843a | ||
![]() |
e68236bd84 | ||
![]() |
447b6f9e5f | ||
![]() |
746bc322b7 | ||
![]() |
a6aee82fcf | ||
![]() |
6095b33ab2 | ||
![]() |
5ac094e51b | ||
![]() |
bdf013d547 | ||
![]() |
96a2dda153 | ||
![]() |
d69d2c8e8d | ||
![]() |
8736bb42f2 | ||
![]() |
effd78dc98 | ||
![]() |
634ce0fa49 | ||
![]() |
6e6a5222cd | ||
![]() |
ce5a9630ca | ||
![]() |
d123722616 |
@@ -1,4 +1,4 @@
|
|||||||
name: API report (misskey.js)
|
name: API report
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
58
.github/workflows/storybook.yml
vendored
58
.github/workflows/storybook.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
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
|
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:6
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
2
.github/workflows/test-frontend.yml
vendored
2
.github/workflows/test-frontend.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:6
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -56,7 +56,6 @@ api-docs.json
|
|||||||
/files
|
/files
|
||||||
ormconfig.json
|
ormconfig.json
|
||||||
temp
|
temp
|
||||||
/packages/frontend/src/**/*.stories.ts
|
|
||||||
|
|
||||||
# blender backups
|
# blender backups
|
||||||
*.blend1
|
*.blend1
|
||||||
|
38
CHANGELOG.md
38
CHANGELOG.md
@@ -5,51 +5,15 @@
|
|||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
-
|
-
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 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
|
## 13.10.3
|
||||||
|
|
||||||
### Changes
|
|
||||||
- オブジェクトストレージのリージョン指定が必須になりました
|
|
||||||
- リージョンの指定の無いサービスは us-east-1 を設定してください
|
|
||||||
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
|
|
||||||
- e.g. ~/aws/config, AWS_REGION
|
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
|
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
|
||||||
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
|
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
|
||||||
|
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -203,116 +203,6 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||||||
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
|
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
|
## Notes
|
||||||
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
||||||
|
|
||||||
|
11
README.md
11
README.md
@@ -54,17 +54,6 @@ 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.
|
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
|
## Sponsors
|
||||||
|
|
||||||
<div align="center">
|
<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>
|
<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>
|
</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.
|
|
||||||
|
@@ -460,7 +460,7 @@ aboutX: "Über {x}"
|
|||||||
emojiStyle: "Emoji-Stil"
|
emojiStyle: "Emoji-Stil"
|
||||||
native: "Nativ"
|
native: "Nativ"
|
||||||
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
|
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
|
||||||
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
|
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
|
||||||
noHistory: "Kein Verlauf gefunden"
|
noHistory: "Kein Verlauf gefunden"
|
||||||
signinHistory: "Anmeldungsverlauf"
|
signinHistory: "Anmeldungsverlauf"
|
||||||
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
||||||
@@ -980,9 +980,6 @@ drivecleaner: "Drive-Reiniger"
|
|||||||
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
|
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
|
||||||
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
|
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
|
||||||
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
|
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:
|
_achievements:
|
||||||
earnedAt: "Freigeschaltet am"
|
earnedAt: "Freigeschaltet am"
|
||||||
_types:
|
_types:
|
||||||
@@ -1880,17 +1877,6 @@ _drivecleaner:
|
|||||||
orderBySizeDesc: "Absteigende Dateigrößen"
|
orderBySizeDesc: "Absteigende Dateigrößen"
|
||||||
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
|
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
createWebhook: "Webhook erstellen"
|
|
||||||
name: "Name"
|
name: "Name"
|
||||||
secret: "Secret"
|
|
||||||
events: "Webhook-Ereignisse"
|
|
||||||
active: "Aktiviert"
|
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"
|
|
||||||
|
|
||||||
|
@@ -500,13 +500,12 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
|
|||||||
objectStorageEndpoint: "Endpoint"
|
objectStorageEndpoint: "Endpoint"
|
||||||
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
||||||
objectStorageRegion: "Region"
|
objectStorageRegion: "Region"
|
||||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
|
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
|
||||||
objectStorageUseSSL: "SSLを使用する"
|
objectStorageUseSSL: "SSLを使用する"
|
||||||
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
||||||
objectStorageUseProxy: "Proxyを利用する"
|
objectStorageUseProxy: "Proxyを利用する"
|
||||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||||
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
|
|
||||||
serverLogs: "サーバーログ"
|
serverLogs: "サーバーログ"
|
||||||
deleteAll: "全て削除"
|
deleteAll: "全て削除"
|
||||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||||
@@ -961,9 +960,7 @@ copyErrorInfo: "エラー情報をコピー"
|
|||||||
joinThisServer: "このサーバーに登録する"
|
joinThisServer: "このサーバーに登録する"
|
||||||
exploreOtherServers: "他のサーバーを探す"
|
exploreOtherServers: "他のサーバーを探す"
|
||||||
letsLookAtTimeline: "タイムラインを見てみる"
|
letsLookAtTimeline: "タイムラインを見てみる"
|
||||||
disableFederationConfirm: "連合なしにしますか?"
|
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
|
||||||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
|
||||||
disableFederationOk: "連合なしにする"
|
|
||||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||||
postToTheChannel: "チャンネルに投稿"
|
postToTheChannel: "チャンネルに投稿"
|
||||||
@@ -986,8 +983,6 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ
|
|||||||
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
|
||||||
noteIdOrUrl: "ノートIDまたはURL"
|
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
|
@@ -129,7 +129,6 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
|
|||||||
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
||||||
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
||||||
selectList: "Wybierz listę"
|
selectList: "Wybierz listę"
|
||||||
selectChannel: "Wybierz kanał"
|
|
||||||
selectAntenna: "Wybierz Antennę"
|
selectAntenna: "Wybierz Antennę"
|
||||||
selectWidget: "Wybierz widżet"
|
selectWidget: "Wybierz widżet"
|
||||||
editWidgets: "Edytuj widżety"
|
editWidgets: "Edytuj widżety"
|
||||||
@@ -150,7 +149,6 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
|
|||||||
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
|
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
|
||||||
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
||||||
addAccount: "Dodaj konto"
|
addAccount: "Dodaj konto"
|
||||||
reloadAccountsList: "Odśwież listę kont"
|
|
||||||
loginFailed: "Nie udało się zalogować"
|
loginFailed: "Nie udało się zalogować"
|
||||||
showOnRemote: "Zobacz na zdalnej instancji"
|
showOnRemote: "Zobacz na zdalnej instancji"
|
||||||
general: "Ogólne"
|
general: "Ogólne"
|
||||||
@@ -161,7 +159,6 @@ searchWith: "Szukaj: {q}"
|
|||||||
youHaveNoLists: "Nie masz żadnej listy"
|
youHaveNoLists: "Nie masz żadnej listy"
|
||||||
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
||||||
proxyAccount: "Konto proxy"
|
proxyAccount: "Konto proxy"
|
||||||
proxyAccountDescription: "Opis konta pełnomocniczego"
|
|
||||||
host: "Host"
|
host: "Host"
|
||||||
selectUser: "Wybierz użytkownika"
|
selectUser: "Wybierz użytkownika"
|
||||||
recipient: "Odbiorca"
|
recipient: "Odbiorca"
|
||||||
@@ -256,7 +253,6 @@ noMoreHistory: "Nie ma dalszej historii"
|
|||||||
startMessaging: "Rozpocznij czat"
|
startMessaging: "Rozpocznij czat"
|
||||||
nUsersRead: "przeczytano przez {n}"
|
nUsersRead: "przeczytano przez {n}"
|
||||||
agreeTo: "Wyrażam zgodę na {0}"
|
agreeTo: "Wyrażam zgodę na {0}"
|
||||||
agreeBelow: "Zaakceptuj poniżej"
|
|
||||||
tos: "Regulamin"
|
tos: "Regulamin"
|
||||||
start: "Rozpocznij"
|
start: "Rozpocznij"
|
||||||
home: "Strona główna"
|
home: "Strona główna"
|
||||||
@@ -389,19 +385,13 @@ about: "Informacje"
|
|||||||
aboutMisskey: "O Misskey"
|
aboutMisskey: "O Misskey"
|
||||||
administrator: "Admin"
|
administrator: "Admin"
|
||||||
token: "Token"
|
token: "Token"
|
||||||
2fa: "Klucz 2FA "
|
|
||||||
totp: "Klucz aplikacji uwierzytelniającej (totp)"
|
|
||||||
totpDescription: "Opis klucza czasowego"
|
|
||||||
moderator: "Moderator"
|
moderator: "Moderator"
|
||||||
moderation: "Moderacja"
|
moderation: "Moderacja"
|
||||||
nUsersMentioned: "{n} wspomnianych użytkowników"
|
nUsersMentioned: "{n} wspomnianych użytkowników"
|
||||||
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
|
|
||||||
securityKey: "Klucz bezpieczeństwa"
|
securityKey: "Klucz bezpieczeństwa"
|
||||||
lastUsed: "Ostatnio używane"
|
lastUsed: "Ostatnio używane"
|
||||||
lastUsedAt: "Ostatnio używane w"
|
|
||||||
unregister: "Cofnij rejestrację"
|
unregister: "Cofnij rejestrację"
|
||||||
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
||||||
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
|
|
||||||
resetPassword: "Zresetuj hasło"
|
resetPassword: "Zresetuj hasło"
|
||||||
newPasswordIs: "Nowe hasło to „{password}”"
|
newPasswordIs: "Nowe hasło to „{password}”"
|
||||||
reduceUiAnimation: "Ogranicz animacje w UI"
|
reduceUiAnimation: "Ogranicz animacje w UI"
|
||||||
@@ -528,16 +518,11 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
|
|||||||
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
||||||
deleteAllFiles: "Usuń wszystkie pliki"
|
deleteAllFiles: "Usuń wszystkie pliki"
|
||||||
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć 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."
|
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
|
||||||
userSuspended: "To konto zostało zawieszone."
|
userSuspended: "To konto zostało zawieszone."
|
||||||
userSilenced: "Ten użytkownik został wyciszony."
|
userSilenced: "Ten użytkownik został wyciszony."
|
||||||
yourAccountSuspendedTitle: "To konto jest zawieszone"
|
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."
|
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"
|
menu: "Menu"
|
||||||
divider: "Rozdzielacz"
|
divider: "Rozdzielacz"
|
||||||
addItem: "Dodaj element"
|
addItem: "Dodaj element"
|
||||||
@@ -563,9 +548,7 @@ author: "Autor"
|
|||||||
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
||||||
manage: "Zarządzanie"
|
manage: "Zarządzanie"
|
||||||
plugins: "Wtyczki"
|
plugins: "Wtyczki"
|
||||||
preferencesBackups: "Kopia zapasowa ustawień"
|
|
||||||
deck: "Tablica"
|
deck: "Tablica"
|
||||||
undeck: "oddkouj"
|
|
||||||
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
||||||
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
||||||
width: "Szerokość"
|
width: "Szerokość"
|
||||||
@@ -832,8 +815,6 @@ tenMinutes: "10 minut"
|
|||||||
oneHour: "1 godzina"
|
oneHour: "1 godzina"
|
||||||
oneDay: "1 dzień"
|
oneDay: "1 dzień"
|
||||||
oneWeek: "1 tydzień"
|
oneWeek: "1 tydzień"
|
||||||
oneMonth: "jeden miesiąc"
|
|
||||||
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
|
|
||||||
file: "Pliki"
|
file: "Pliki"
|
||||||
recommended: "Zalecane"
|
recommended: "Zalecane"
|
||||||
check: "Zweryfikuj"
|
check: "Zweryfikuj"
|
||||||
|
@@ -980,9 +980,6 @@ drivecleaner: "网盘整理"
|
|||||||
retryAllQueuesNow: "立刻重试所有队列"
|
retryAllQueuesNow: "立刻重试所有队列"
|
||||||
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
|
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
|
||||||
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
|
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
|
||||||
enableChartsForRemoteUser: "生成远程用户的图表"
|
|
||||||
enableChartsForFederatedInstances: "生成远程服务器的图表"
|
|
||||||
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "达成时间"
|
earnedAt: "达成时间"
|
||||||
_types:
|
_types:
|
||||||
@@ -1279,8 +1276,6 @@ _role:
|
|||||||
followersMoreThanOrEq: "关注者不少于"
|
followersMoreThanOrEq: "关注者不少于"
|
||||||
followingLessThanOrEq: "关注中不多于"
|
followingLessThanOrEq: "关注中不多于"
|
||||||
followingMoreThanOrEq: "关注中不少于"
|
followingMoreThanOrEq: "关注中不少于"
|
||||||
notesLessThanOrEq: "帖子数在~以下"
|
|
||||||
notesMoreThanOrEq: "帖子数在~以上"
|
|
||||||
and: "符合以下全部条件"
|
and: "符合以下全部条件"
|
||||||
or: "符合以下任一条件"
|
or: "符合以下任一条件"
|
||||||
not: "不符合以下任何条件"
|
not: "不符合以下任何条件"
|
||||||
@@ -1880,17 +1875,6 @@ _drivecleaner:
|
|||||||
orderBySizeDesc: "按大小降序排列"
|
orderBySizeDesc: "按大小降序排列"
|
||||||
orderByCreatedAtAsc: "按添加日期降序排列"
|
orderByCreatedAtAsc: "按添加日期降序排列"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
createWebhook: "创建 Webhook"
|
|
||||||
name: "名称"
|
name: "名称"
|
||||||
secret: "密钥"
|
|
||||||
events: "何时运行Webhook"
|
|
||||||
active: "已启用"
|
active: "已启用"
|
||||||
_events:
|
|
||||||
follow: "关注时"
|
|
||||||
followed: "被关注时"
|
|
||||||
note: "发布贴文时"
|
|
||||||
reply: "收到回复时"
|
|
||||||
renote: "被转发时"
|
|
||||||
reaction: "被回应时"
|
|
||||||
mention: "被提及时"
|
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ gotIt: "知道了"
|
|||||||
cancel: "取消"
|
cancel: "取消"
|
||||||
noThankYou: "現在不要"
|
noThankYou: "現在不要"
|
||||||
enterUsername: "輸入使用者名稱"
|
enterUsername: "輸入使用者名稱"
|
||||||
renotedBy: "{user} 轉發了"
|
renotedBy: "{user} 轉傳了"
|
||||||
noNotes: "無貼文。"
|
noNotes: "無貼文。"
|
||||||
noNotifications: "沒有通知"
|
noNotifications: "沒有通知"
|
||||||
instance: "實例"
|
instance: "實例"
|
||||||
@@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
|
|||||||
enterEmoji: "輸入表情符號"
|
enterEmoji: "輸入表情符號"
|
||||||
renote: "轉發"
|
renote: "轉發"
|
||||||
unrenote: "取消轉發"
|
unrenote: "取消轉發"
|
||||||
renoted: "轉發成功"
|
renoted: "轉傳成功"
|
||||||
cantRenote: "無法轉發此貼文。"
|
cantRenote: "無法轉發此貼文。"
|
||||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
inChannelRenote: "在頻道內轉發"
|
inChannelRenote: "在頻道內轉發"
|
||||||
inChannelQuote: "在頻道內引用"
|
inChannelQuote: "在頻道內引用"
|
||||||
@@ -980,9 +980,6 @@ drivecleaner: "雲端硬碟清掃器"
|
|||||||
retryAllQueuesNow: "立刻重試所有佇列"
|
retryAllQueuesNow: "立刻重試所有佇列"
|
||||||
retryAllQueuesConfirmTitle: "要現在重試嗎?"
|
retryAllQueuesConfirmTitle: "要現在重試嗎?"
|
||||||
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
|
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
|
||||||
enableChartsForRemoteUser: "生成遠端用戶的圖表"
|
|
||||||
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
|
|
||||||
showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日期"
|
earnedAt: "獲得日期"
|
||||||
_types:
|
_types:
|
||||||
@@ -1099,7 +1096,7 @@ _achievements:
|
|||||||
title: "有備而來"
|
title: "有備而來"
|
||||||
description: "設定了個人檔案"
|
description: "設定了個人檔案"
|
||||||
_markedAsCat:
|
_markedAsCat:
|
||||||
title: "吾輩乃貓是也"
|
title: "我是貓"
|
||||||
description: "已將帳戶設定為貓"
|
description: "已將帳戶設定為貓"
|
||||||
flavor: "還沒有名字。"
|
flavor: "還沒有名字。"
|
||||||
_following1:
|
_following1:
|
||||||
@@ -1279,8 +1276,6 @@ _role:
|
|||||||
followersMoreThanOrEq: "追隨者人數在~以上"
|
followersMoreThanOrEq: "追隨者人數在~以上"
|
||||||
followingLessThanOrEq: "追隨人數在~以下"
|
followingLessThanOrEq: "追隨人數在~以下"
|
||||||
followingMoreThanOrEq: "追隨人數在~以上"
|
followingMoreThanOrEq: "追隨人數在~以上"
|
||||||
notesLessThanOrEq: "發布數在~以下"
|
|
||||||
notesMoreThanOrEq: "發布數在~以上"
|
|
||||||
and: "~和~"
|
and: "~和~"
|
||||||
or: "~或~"
|
or: "~或~"
|
||||||
not: "~否"
|
not: "~否"
|
||||||
@@ -1880,17 +1875,6 @@ _drivecleaner:
|
|||||||
orderBySizeDesc: "檔案由大到小"
|
orderBySizeDesc: "檔案由大到小"
|
||||||
orderByCreatedAtAsc: "依照加入的日期順序"
|
orderByCreatedAtAsc: "依照加入的日期順序"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
createWebhook: "建立 Webhook"
|
|
||||||
name: "名稱"
|
name: "名稱"
|
||||||
secret: "秘密"
|
|
||||||
events: "什麼時候運行Webhook"
|
|
||||||
active: "已啟用"
|
active: "已啟用"
|
||||||
_events:
|
|
||||||
follow: "當你追隨時"
|
|
||||||
followed: "當被追隨時"
|
|
||||||
note: "當發布貼文時"
|
|
||||||
reply: "當收到回覆時"
|
|
||||||
renote: "當被轉發時"
|
|
||||||
reaction: "當獲得反應時"
|
|
||||||
mention: "當被提到時"
|
|
||||||
|
|
||||||
|
14
package.json
14
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.11.0.beta-2",
|
"version": "13.10.3",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.1.0",
|
"packageManager": "pnpm@7.29.3",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
@@ -50,16 +50,16 @@
|
|||||||
"gulp-replace": "1.1.4",
|
"gulp-replace": "1.1.4",
|
||||||
"gulp-terser": "2.1.0",
|
"gulp-terser": "2.1.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"typescript": "5.0.2"
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||||
"@typescript-eslint/parser": "5.57.0",
|
"@typescript-eslint/parser": "5.54.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.9.0",
|
"cypress": "12.7.0",
|
||||||
"eslint": "8.37.0",
|
"eslint": "8.35.0",
|
||||||
"start-server-and-test": "2.0.0"
|
"start-server-and-test": "2.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
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"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
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"`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
export class cleanup1680491187535 {
|
|
||||||
name = 'cleanup1680491187535'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`DROP TABLE "antenna_note" `);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
export class cleanup1680582195041 {
|
|
||||||
name = 'cleanup1680582195041'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`DROP TABLE "notification" `);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -23,41 +23,41 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "^1.3.11",
|
"@swc/core-android-arm64": "^1.3.11",
|
||||||
"@swc/core-darwin-arm64": "^1.3.42",
|
"@swc/core-darwin-arm64": "^1.3.38",
|
||||||
"@swc/core-darwin-x64": "^1.3.42",
|
"@swc/core-darwin-x64": "^1.3.38",
|
||||||
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
|
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
|
||||||
"@swc/core-linux-arm64-gnu": "^1.3.42",
|
"@swc/core-linux-arm64-gnu": "^1.3.38",
|
||||||
"@swc/core-linux-arm64-musl": "^1.3.42",
|
"@swc/core-linux-arm64-musl": "^1.3.38",
|
||||||
"@swc/core-linux-x64-gnu": "^1.3.42",
|
"@swc/core-linux-x64-gnu": "^1.3.38",
|
||||||
"@swc/core-linux-x64-musl": "^1.3.42",
|
"@swc/core-linux-x64-musl": "^1.3.38",
|
||||||
"@swc/core-win32-arm64-msvc": "^1.3.42",
|
"@swc/core-win32-arm64-msvc": "^1.3.38",
|
||||||
"@swc/core-win32-ia32-msvc": "^1.3.42",
|
"@swc/core-win32-ia32-msvc": "^1.3.38",
|
||||||
"@swc/core-win32-x64-msvc": "^1.3.42",
|
"@swc/core-win32-x64-msvc": "^1.3.38",
|
||||||
"@tensorflow/tfjs": "4.2.0",
|
"@tensorflow/tfjs": "4.2.0",
|
||||||
"@tensorflow/tfjs-node": "4.2.0"
|
"@tensorflow/tfjs-node": "4.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.301.0",
|
"@aws-sdk/client-s3": "^3.294.0",
|
||||||
"@aws-sdk/lib-storage": "3.301.0",
|
"@aws-sdk/lib-storage": "^3.294.0",
|
||||||
"@aws-sdk/node-http-handler": "3.296.0",
|
"@aws-sdk/node-http-handler": "^3.292.0",
|
||||||
"@bull-board/api": "5.0.0",
|
"@bull-board/api": "5.0.0",
|
||||||
"@bull-board/fastify": "5.0.0",
|
"@bull-board/fastify": "5.0.0",
|
||||||
"@bull-board/ui": "5.0.0",
|
"@bull-board/ui": "5.0.0",
|
||||||
"@discordapp/twemoji": "14.1.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.2.1",
|
"@fastify/cors": "8.2.0",
|
||||||
"@fastify/http-proxy": "9.0.0",
|
"@fastify/http-proxy": "8.4.0",
|
||||||
"@fastify/multipart": "7.5.0",
|
"@fastify/multipart": "7.4.2",
|
||||||
"@fastify/static": "6.9.0",
|
"@fastify/static": "6.9.0",
|
||||||
"@fastify/view": "7.4.1",
|
"@fastify/view": "7.4.1",
|
||||||
"@nestjs/common": "9.3.12",
|
"@nestjs/common": "9.3.9",
|
||||||
"@nestjs/core": "9.3.12",
|
"@nestjs/core": "9.3.9",
|
||||||
"@nestjs/testing": "9.3.12",
|
"@nestjs/testing": "9.3.9",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.0.2",
|
"@sinonjs/fake-timers": "10.0.2",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.42",
|
"@swc/core": "1.3.38",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"fastify": "4.15.0",
|
"fastify": "4.14.1",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "18.2.1",
|
"file-type": "18.2.1",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
@@ -88,21 +88,21 @@
|
|||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "3.1.0",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "21.1.1",
|
"jsdom": "21.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.1.1",
|
"jsonld": "8.1.1",
|
||||||
"jsrsasign": "10.7.0",
|
"jsrsasign": "10.6.1",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "../misskey-js",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.1",
|
"node-fetch": "3.3.0",
|
||||||
"nodemailer": "6.9.1",
|
"nodemailer": "6.9.1",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.1.1",
|
"otpauth": "^9.0.2",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.10.0",
|
"pg": "8.10.0",
|
||||||
"private-ip": "3.0.0",
|
"private-ip": "3.0.0",
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
"sanitize-html": "2.10.0",
|
"sanitize-html": "2.10.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"semver": "7.3.8",
|
"semver": "7.3.8",
|
||||||
"sharp": "0.32.0",
|
"sharp": "0.31.3",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
@@ -133,25 +133,25 @@
|
|||||||
"systeminformation": "5.17.12",
|
"systeminformation": "5.17.12",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.5",
|
"tsc-alias": "1.8.3",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.1.2",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.11",
|
||||||
"typescript": "5.0.2",
|
"typescript": "4.9.5",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"websocket": "1.0.34",
|
"websocket": "1.0.34",
|
||||||
"ws": "8.13.0",
|
"ws": "8.12.1",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.5.0",
|
"@jest/globals": "29.5.0",
|
||||||
"@swc/jest": "0.2.24",
|
"@swc/jest": "0.2.24",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.2",
|
"@types/archiver": "5.3.1",
|
||||||
"@types/bcryptjs": "2.4.2",
|
"@types/bcryptjs": "2.4.2",
|
||||||
"@types/bull": "4.10.0",
|
"@types/bull": "4.10.0",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.21",
|
"@types/fluent-ffmpeg": "2.1.21",
|
||||||
"@types/ioredis": "4.28.10",
|
"@types/ioredis": "4.28.10",
|
||||||
"@types/jest": "29.5.0",
|
"@types/jest": "29.4.0",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.1",
|
"@types/jsdom": "21.1.0",
|
||||||
"@types/jsonld": "1.5.8",
|
"@types/jsonld": "1.5.8",
|
||||||
"@types/jsrsasign": "10.5.8",
|
"@types/jsrsasign": "10.5.5",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/node": "18.15.11",
|
"@types/node": "18.15.0",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"@types/ratelimiter": "3.4.4",
|
"@types/ratelimiter": "3.4.4",
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.4",
|
||||||
"@types/sanitize-html": "2.9.0",
|
"@types/sanitize-html": "2.8.1",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
"@types/sharp": "0.31.1",
|
"@types/sharp": "0.31.1",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
@@ -190,11 +190,11 @@
|
|||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||||
"@typescript-eslint/parser": "5.57.0",
|
"@typescript-eslint/parser": "5.54.1",
|
||||||
"aws-sdk-client-mock": "^2.1.1",
|
"aws-sdk-client-mock": "^2.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.37.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
|
@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
|
|||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
@@ -24,9 +24,6 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
private antennas: Antenna[];
|
private antennas: Antenna[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisSubscriber)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisSubscriber: Redis.Redis,
|
||||||
|
|
||||||
@@ -36,6 +33,9 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.antennaNotesRepository)
|
||||||
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@@ -92,13 +92,54 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||||
this.redisClient.xadd(
|
// 通知しない設定になっているか、自分自身の投稿なら既読にする
|
||||||
`antennaTimeline:${antenna.id}`,
|
const read = !antenna.notify || (antenna.userId === noteUser.id);
|
||||||
'MAXLEN', '~', '200',
|
|
||||||
`${this.idService.parse(note.id).date.getTime()}-*`,
|
this.antennaNotesRepository.insert({
|
||||||
'note', note.id);
|
id: this.idService.genId(),
|
||||||
|
antennaId: antenna.id,
|
||||||
|
noteId: note.id,
|
||||||
|
read: read,
|
||||||
|
});
|
||||||
|
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
@@ -1,172 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -38,9 +38,9 @@ import { S3Service } from './S3Service.js';
|
|||||||
import { SignupService } from './SignupService.js';
|
import { SignupService } from './SignupService.js';
|
||||||
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { CacheService } from './CacheService.js';
|
import { UserCacheService } from './UserCacheService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairService } from './UserKeypairService.js';
|
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
import { UserMutingService } from './UserMutingService.js';
|
import { UserMutingService } from './UserMutingService.js';
|
||||||
import { UserSuspendService } from './UserSuspendService.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 $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
|
||||||
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
@@ -282,9 +282,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SignupService,
|
SignupService,
|
||||||
TwoFactorAuthenticationService,
|
TwoFactorAuthenticationService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
UserCacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairStoreService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
@@ -399,9 +399,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SignupService,
|
$SignupService,
|
||||||
$TwoFactorAuthenticationService,
|
$TwoFactorAuthenticationService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$UserCacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairStoreService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
@@ -517,9 +517,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SignupService,
|
SignupService,
|
||||||
TwoFactorAuthenticationService,
|
TwoFactorAuthenticationService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
UserCacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairStoreService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
@@ -633,9 +633,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SignupService,
|
$SignupService,
|
||||||
$TwoFactorAuthenticationService,
|
$TwoFactorAuthenticationService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$UserCacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairStoreService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
|
@@ -1,28 +1,24 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository, Note } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { ReactionService } from '@/core/ReactionService.js';
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService {
|
export class CustomEmojiService {
|
||||||
private cache: MemoryKVCache<Emoji | null>;
|
private cache: KVCache<Emoji | null>;
|
||||||
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@@ -36,16 +32,9 @@ export class CustomEmojiService {
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private reactionService: ReactionService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
|
this.cache = new KVCache<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
|
@bindThis
|
||||||
@@ -71,7 +60,7 @@ export class CustomEmojiService {
|
|||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
this.localEmojisCache.refresh();
|
await this.db.queryResultCache?.remove(['meta_emojis']);
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||||
@@ -81,146 +70,6 @@ export class CustomEmojiService {
|
|||||||
return emoji;
|
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
|
@bindThis
|
||||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||||
// クエリに使うホスト
|
// クエリに使うホスト
|
||||||
@@ -235,7 +84,7 @@ export class CustomEmojiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||||
if (!match) return { name: null, host: null };
|
if (!match) return { name: null, host: null };
|
||||||
|
|
||||||
@@ -294,6 +143,30 @@ export class CustomEmojiService {
|
|||||||
return res;
|
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; }[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||||
*/
|
*/
|
||||||
|
@@ -36,5 +36,8 @@ export class DeleteAccountService {
|
|||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Terminate streaming
|
||||||
|
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { InstancesRepository } from '@/models/index.js';
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
@@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FederatedInstanceService {
|
export class FederatedInstanceService {
|
||||||
private cache: MemoryKVCache<Instance>;
|
private cache: KVCache<Instance>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
@@ -18,7 +18,7 @@ export class FederatedInstanceService {
|
|||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
|
this.cache = new KVCache<Instance>(1000 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -14,6 +14,7 @@ import type {
|
|||||||
MainStreamTypes,
|
MainStreamTypes,
|
||||||
NoteStreamTypes,
|
NoteStreamTypes,
|
||||||
UserListStreamTypes,
|
UserListStreamTypes,
|
||||||
|
UserStreamTypes,
|
||||||
} from '@/server/api/stream/types.js';
|
} from '@/server/api/stream/types.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -48,6 +49,11 @@ export class GlobalEventService {
|
|||||||
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
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
|
@bindThis
|
||||||
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
||||||
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
||||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { ulid } from 'ulid';
|
import { ulid } from 'ulid';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { genAid, parseAid } from '@/misc/id/aid.js';
|
import { genAid } from '@/misc/id/aid.js';
|
||||||
import { genMeid } from '@/misc/id/meid.js';
|
import { genMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId } from '@/misc/id/object-id.js';
|
||||||
@@ -32,17 +32,4 @@ export class IdService {
|
|||||||
default: throw new Error('unrecognized id generation method');
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { UsersRepository } from '@/models/index.js';
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InstanceActorService {
|
export class InstanceActorService {
|
||||||
private cache: MemorySingleCache<LocalUser>;
|
private cache: KVCache<LocalUser>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
@@ -19,12 +19,12 @@ export class InstanceActorService {
|
|||||||
|
|
||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemorySingleCache<LocalUser>(Infinity);
|
this.cache = new KVCache<LocalUser>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getInstanceActor(): Promise<LocalUser> {
|
public async getInstanceActor(): Promise<LocalUser> {
|
||||||
const cached = this.cache.get();
|
const cached = this.cache.get(null);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
@@ -33,11 +33,11 @@ export class InstanceActorService {
|
|||||||
}) as LocalUser | undefined;
|
}) as LocalUser | undefined;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.cache.set(user);
|
this.cache.set(null, user);
|
||||||
return user;
|
return user;
|
||||||
} else {
|
} else {
|
||||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
||||||
this.cache.set(created);
|
this.cache.set(null, created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource } from 'typeorm';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
@@ -20,7 +19,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
|||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
import type { Channel } from '@/models/entities/Channel.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
@@ -47,7 +46,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -151,9 +150,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -325,14 +321,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
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(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||||
() => { /* aborted, ignore this */ },
|
() => { /* aborted, ignore this */ },
|
||||||
@@ -473,7 +461,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
this.incNotesCountOfUser(user);
|
this.incNotesCountOfUser(user);
|
||||||
|
|
||||||
// Word mute
|
// Word mute
|
||||||
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
|
||||||
where: {
|
where: {
|
||||||
enableWordMute: true,
|
enableWordMute: true,
|
||||||
},
|
},
|
||||||
@@ -502,6 +490,18 @@ 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) {
|
if (data.reply) {
|
||||||
this.saveReply(data.reply, note);
|
this.saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,28 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In, IsNull, Not } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { User } from '@/models/entities/User.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 { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
|
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { NotificationService } from './NotificationService.js';
|
||||||
|
import { AntennaService } from './AntennaService.js';
|
||||||
|
import { PushNotificationService } from './PushNotificationService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
@@ -24,8 +32,21 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.noteThreadMutingsRepository)
|
@Inject(DI.noteThreadMutingsRepository)
|
||||||
private noteThreadMutingsRepository: 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 idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
|
private antennaService: AntennaService,
|
||||||
|
private pushNotificationService: PushNotificationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +57,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
isMentioned: boolean;
|
isMentioned: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
//#region ミュートしているなら無視
|
//#region ミュートしているなら無視
|
||||||
|
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
|
||||||
const mute = await this.mutingsRepository.findBy({
|
const mute = await this.mutingsRepository.findBy({
|
||||||
muterId: userId,
|
muterId: userId,
|
||||||
});
|
});
|
||||||
@@ -55,6 +77,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: params.isSpecified,
|
isSpecified: params.isSpecified,
|
||||||
isMentioned: params.isMentioned,
|
isMentioned: params.isMentioned,
|
||||||
|
noteChannelId: note.channelId,
|
||||||
noteUserId: note.userId,
|
noteUserId: note.userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +95,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
if (params.isSpecified) {
|
if (params.isSpecified) {
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||||
}
|
}
|
||||||
|
if (note.channelId) {
|
||||||
|
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
||||||
|
}
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +105,23 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
public async read(
|
public async read(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
notes: (Note | Packed<'Note'>)[],
|
notes: (Note | Packed<'Note'>)[],
|
||||||
|
info?: {
|
||||||
|
following: Set<User['id']>;
|
||||||
|
followingChannels: Set<Channel['id']>;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): 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 readMentions: (Note | Packed<'Note'>)[] = [];
|
||||||
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
|
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
|
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.mentions && note.mentions.includes(userId)) {
|
if (note.mentions && note.mentions.includes(userId)) {
|
||||||
@@ -89,13 +129,25 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||||
readSpecifiedNotes.push(note);
|
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)) {
|
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
|
||||||
// Remove the record
|
// Remove the record
|
||||||
await this.noteUnreadsRepository.delete({
|
await this.noteUnreadsRepository.delete({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
@@ -119,6 +171,49 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
@@ -12,22 +11,21 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notificationsRepository)
|
||||||
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@@ -36,36 +34,54 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
private cacheService: CacheService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async readAllNotification(
|
public async readNotification(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
force = false,
|
notificationIds: Notification['id'][],
|
||||||
) {
|
) {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
if (notificationIds.length === 0) return;
|
||||||
|
|
||||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
|
||||||
`notificationTimeline:${userId}`,
|
|
||||||
'+',
|
|
||||||
'-',
|
|
||||||
'COUNT', 1);
|
|
||||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
|
||||||
|
|
||||||
if (latestNotificationId == null) return;
|
// Update documents
|
||||||
|
const result = await this.notificationsRepository.update({
|
||||||
|
notifieeId: userId,
|
||||||
|
id: In(notificationIds),
|
||||||
|
isRead: false,
|
||||||
|
}, {
|
||||||
|
isRead: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
if (result.affected === 0) return;
|
||||||
|
|
||||||
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private postReadAllNotifications(userId: User['id']) {
|
private postReadAllNotifications(userId: User['id']) {
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
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
|
@bindThis
|
||||||
@@ -74,43 +90,45 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
type: Notification['type'],
|
type: Notification['type'],
|
||||||
data: Partial<Notification>,
|
data: Partial<Notification>,
|
||||||
): Promise<Notification | null> {
|
): Promise<Notification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
if (data.notifierId && (notifieeId === data.notifierId)) {
|
||||||
const isMuted = profile.mutingNotificationTypes.includes(type);
|
return null;
|
||||||
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 notification = {
|
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
|
||||||
|
|
||||||
|
const isMuted = profile?.mutingNotificationTypes.includes(type);
|
||||||
|
|
||||||
|
// Create notification
|
||||||
|
const notification = await this.notificationsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
notifieeId: notifieeId,
|
||||||
type: type,
|
type: type,
|
||||||
|
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
|
||||||
|
isRead: isMuted,
|
||||||
...data,
|
...data,
|
||||||
} as Notification;
|
} as Partial<Notification>)
|
||||||
|
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
const redisIdPromise = this.redisClient.xadd(
|
const packed = await this.notificationEntityService.pack(notification, {});
|
||||||
`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
|
// Publish notification event
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
|
||||||
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
|
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
|
||||||
|
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
@@ -15,6 +15,10 @@ type PushNotificationsTypes = {
|
|||||||
antenna: { id: string, name: string };
|
antenna: { id: string, name: string };
|
||||||
note: Packed<'Note'>;
|
note: Packed<'Note'>;
|
||||||
};
|
};
|
||||||
|
'readNotifications': { notificationIds: string[] };
|
||||||
|
'readAllNotifications': undefined;
|
||||||
|
'readAntenna': { antennaId: string };
|
||||||
|
'readAllAntennas': undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reduce length because push message servers have character limits
|
// Reduce length because push message servers have character limits
|
||||||
@@ -68,6 +72,14 @@ export class PushNotificationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
|
// Continue if sendReadMessage is false
|
||||||
|
if ([
|
||||||
|
'readNotifications',
|
||||||
|
'readAllNotifications',
|
||||||
|
'readAntenna',
|
||||||
|
'readAllAntennas',
|
||||||
|
].includes(type) && !subscription.sendReadMessage) continue;
|
||||||
|
|
||||||
const pushSubscription = {
|
const pushSubscription = {
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
keys: {
|
keys: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
@@ -19,7 +20,6 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
|
|
||||||
@@ -60,6 +60,9 @@ export class ReactionService {
|
|||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.blockingsRepository)
|
||||||
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@@ -71,7 +74,6 @@ export class ReactionService {
|
|||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private customEmojiService: CustomEmojiService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
@@ -102,6 +104,7 @@ export class ReactionService {
|
|||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '❤️';
|
reaction = '❤️';
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: cache
|
||||||
reaction = await this.toDbReaction(reaction, user.host);
|
reaction = await this.toDbReaction(reaction, user.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,22 +158,20 @@ export class ReactionService {
|
|||||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
const decodedReaction = this.decodeReaction(reaction);
|
const decodedReaction = this.decodeReaction(reaction);
|
||||||
|
|
||||||
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
const emoji = await this.emojisRepository.findOne({
|
||||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
where: {
|
||||||
: await this.emojisRepository.findOne(
|
name: decodedReaction.name,
|
||||||
{
|
host: decodedReaction.host ?? IsNull(),
|
||||||
where: {
|
},
|
||||||
name: decodedReaction.name,
|
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||||
host: decodedReaction.host,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: decodedReaction.reaction,
|
reaction: decodedReaction.reaction,
|
||||||
emoji: customEmoji != null ? {
|
emoji: emoji != null ? {
|
||||||
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
} : null,
|
} : null,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
@@ -309,12 +310,10 @@ export class ReactionService {
|
|||||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
const emoji = reacterHost == null
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
host: reacterHost ?? IsNull(),
|
||||||
: await this.emojisRepository.findOneBy({
|
name,
|
||||||
host: reacterHost,
|
});
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
|||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { Relay } from '@/models/entities/Relay.js';
|
import type { Relay } from '@/models/entities/Relay.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RelayService {
|
export class RelayService {
|
||||||
private relaysCache: MemorySingleCache<Relay[]>;
|
private relaysCache: KVCache<Relay[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
@@ -30,7 +30,7 @@ export class RelayService {
|
|||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
) {
|
) {
|
||||||
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
|
this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -109,7 +109,7 @@ export class RelayService {
|
|||||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||||
if (activity == null) return;
|
if (activity == null) return;
|
||||||
|
|
||||||
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
|
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
}));
|
}));
|
||||||
if (relays.length === 0) return;
|
if (relays.length === 0) return;
|
||||||
|
@@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
@@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown {
|
export class RoleService implements OnApplicationShutdown {
|
||||||
private rolesCache: MemorySingleCache<Role[]>;
|
private rolesCache: KVCache<Role[]>;
|
||||||
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>;
|
||||||
|
|
||||||
public static AlreadyAssignedError = class extends Error {};
|
public static AlreadyAssignedError = class extends Error {};
|
||||||
public static NotAssignedError = class extends Error {};
|
public static NotAssignedError = class extends Error {};
|
||||||
@@ -77,15 +77,15 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private cacheService: CacheService,
|
private userCacheService: UserCacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
this.rolesCache = new MemorySingleCache<Role[]>(Infinity);
|
this.rolesCache = new KVCache<Role[]>(Infinity);
|
||||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
|
this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity);
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisSubscriber.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get(null);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
cached.push({
|
cached.push({
|
||||||
...body,
|
...body,
|
||||||
@@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'roleUpdated': {
|
case 'roleUpdated': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get(null);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const i = cached.findIndex(x => x.id === body.id);
|
const i = cached.findIndex(x => x.id === body.id);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
@@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'roleDeleted': {
|
case 'roleDeleted': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get(null);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.rolesCache.set(cached.filter(x => x.id !== body.id));
|
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -214,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
// 期限切れのロールを除外
|
// 期限切れのロールを除外
|
||||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
|
||||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
||||||
return [...assignedRoles, ...matchedCondRoles];
|
return [...assignedRoles, ...matchedCondRoles];
|
||||||
}
|
}
|
||||||
@@ -231,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
// 期限切れのロールを除外
|
// 期限切れのロールを除外
|
||||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||||
if (badgeCondRoles.length > 0) {
|
if (badgeCondRoles.length > 0) {
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
|
||||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
||||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||||
} else {
|
} else {
|
||||||
@@ -301,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
||||||
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||||
roleId: In(moderatorRoles.map(r => r.id)),
|
roleId: In(moderatorRoles.map(r => r.id)),
|
||||||
@@ -321,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getAdministratorIds(): Promise<User['id'][]> {
|
public async getAdministratorIds(): Promise<User['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const administratorRoles = roles.filter(r => r.isAdministrator);
|
const administratorRoles = roles.filter(r => r.isAdministrator);
|
||||||
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||||
roleId: In(administratorRoles.map(r => r.id)),
|
roleId: In(administratorRoles.map(r => r.id)),
|
||||||
|
@@ -42,7 +42,7 @@ export class S3Service {
|
|||||||
accessKeyId: meta.objectStorageAccessKey,
|
accessKeyId: meta.objectStorageAccessKey,
|
||||||
secretAccessKey: meta.objectStorageSecretKey,
|
secretAccessKey: meta.objectStorageSecretKey,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
|
region: meta.objectStorageRegion ?? undefined,
|
||||||
tls: meta.objectStorageUseSSL,
|
tls: meta.objectStorageUseSSL,
|
||||||
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
|
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
|
||||||
requestHandler: new NodeHttpHandler(handlerOption),
|
requestHandler: new NodeHttpHandler(handlerOption),
|
||||||
|
@@ -1,30 +1,40 @@
|
|||||||
|
|
||||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import Redis from 'ioredis';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserBlockingService implements OnModuleInit {
|
export class UserBlockingService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private userFollowingService: UserFollowingService;
|
|
||||||
|
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||||
|
private blockingsByUserIdCache: KVCache<User['id'][]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
@Inject(DI.redisSubscriber)
|
||||||
|
private redisSubscriber: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
@@ -37,20 +47,47 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListJoiningsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('user-block');
|
this.logger = this.loggerService.getLogger('user-block');
|
||||||
|
|
||||||
|
this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
|
||||||
|
|
||||||
|
this.redisSubscriber.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
@bindThis
|
||||||
this.userFollowingService = this.moduleRef.get('UserFollowingService');
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'blockingCreated': {
|
||||||
|
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
||||||
|
if (cached) {
|
||||||
|
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'blockingDeleted': {
|
||||||
|
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
||||||
|
if (cached) {
|
||||||
|
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -58,8 +95,8 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.cancelRequest(blocker, blockee),
|
this.cancelRequest(blocker, blockee),
|
||||||
this.cancelRequest(blockee, blocker),
|
this.cancelRequest(blockee, blocker),
|
||||||
this.userFollowingService.unfollow(blocker, blockee),
|
this.unFollow(blocker, blockee),
|
||||||
this.userFollowingService.unfollow(blockee, blocker),
|
this.unFollow(blockee, blocker),
|
||||||
this.removeFromList(blockee, blocker),
|
this.removeFromList(blockee, blocker),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -74,9 +111,6 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
|
|
||||||
await this.blockingsRepository.insert(blocking);
|
await this.blockingsRepository.insert(blocking);
|
||||||
|
|
||||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
|
||||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
@@ -114,6 +148,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
this.userEntityService.pack(followee, follower, {
|
this.userEntityService.pack(followee, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
@@ -138,6 +173,54 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@bindThis
|
||||||
private async removeFromList(listOwner: User, user: User) {
|
private async removeFromList(listOwner: User, user: User) {
|
||||||
const userLists = await this.userListsRepository.findBy({
|
const userLists = await this.userListsRepository.findBy({
|
||||||
@@ -171,9 +254,6 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
|
|
||||||
await this.blockingsRepository.delete(blocking.id);
|
await this.blockingsRepository.delete(blocking.id);
|
||||||
|
|
||||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
|
||||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
@@ -188,6 +268,17 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||||
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
packages/backend/src/core/UserCacheService.ts
Normal file
88
packages/backend/src/core/UserCacheService.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
@@ -19,7 +18,6 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
@@ -38,12 +36,8 @@ type Remote = RemoteUser | {
|
|||||||
type Both = Local | Remote;
|
type Both = Local | Remote;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFollowingService implements OnModuleInit {
|
export class UserFollowingService {
|
||||||
private userBlockingService: UserBlockingService;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -59,8 +53,8 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private userBlockingService: UserBlockingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
@@ -74,10 +68,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
|
||||||
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
@@ -182,8 +172,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const req = await this.followRequestsRepository.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
@@ -237,6 +225,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
|
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
@@ -290,8 +279,6 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
|
|
||||||
await this.followingsRepository.delete(following.id);
|
await this.followingsRepository.delete(following.id);
|
||||||
|
|
||||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
|
||||||
|
|
||||||
this.decrementFollowing(follower, followee);
|
this.decrementFollowing(follower, followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
// Publish unfollow event
|
||||||
@@ -299,6 +286,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
@@ -591,6 +579,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
24
packages/backend/src/core/UserKeypairStoreService.ts
Normal file
24
packages/backend/src/core/UserKeypairStoreService.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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 }));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,47 +1,34 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import type { UsersRepository, MutingsRepository } from '@/models/index.js';
|
||||||
import type { MutingsRepository, Muting } from '@/models/index.js';
|
|
||||||
import { IdService } from '@/core/IdService.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 type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserMutingService {
|
export class UserMutingService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private queueService: QueueService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
|
public async mute(user: User, target: User): Promise<void> {
|
||||||
await this.mutingsRepository.insert({
|
await this.mutingsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
expiresAt: expiresAt ?? null,
|
|
||||||
muterId: user.id,
|
muterId: user.id,
|
||||||
muteeId: target.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,7 +37,7 @@ export class VideoProcessingService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
|
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280);
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||||
@@ -31,8 +31,8 @@ export type UriParseResult = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApDbResolverService {
|
export class ApDbResolverService {
|
||||||
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
private publicKeyCache: KVCache<UserPublickey | null>;
|
||||||
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
private publicKeyByUserIdCache: KVCache<UserPublickey | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@@ -47,11 +47,11 @@ export class ApDbResolverService {
|
|||||||
@Inject(DI.userPublickeysRepository)
|
@Inject(DI.userPublickeysRepository)
|
||||||
private userPublickeysRepository: UserPublickeysRepository,
|
private userPublickeysRepository: UserPublickeysRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private userCacheService: UserCacheService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
) {
|
) {
|
||||||
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity);
|
||||||
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -107,11 +107,11 @@ export class ApDbResolverService {
|
|||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== 'users') return null;
|
if (parsed.type !== 'users') return null;
|
||||||
|
|
||||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
}).then(x => x ?? undefined)) ?? null;
|
}).then(x => x ?? undefined)) ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export class ApDbResolverService {
|
|||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
|
user: await this.userCacheService.findById(key.userId) as RemoteUser,
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -14,15 +14,13 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
|||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { Poll } from '@/models/entities/Poll.js';
|
import type { Poll } from '@/models/entities/Poll.js';
|
||||||
import type { PollVote } from '@/models/entities/PollVote.js';
|
import type { PollVote } from '@/models/entities/PollVote.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.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 { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
@@ -52,11 +50,10 @@ export class ApRendererService {
|
|||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
private customEmojiService: CustomEmojiService,
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private ldSignatureService: LdSignatureService,
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairStoreService: UserKeypairStoreService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private mfmService: MfmService,
|
private mfmService: MfmService,
|
||||||
) {
|
) {
|
||||||
@@ -275,7 +272,11 @@ export class ApRendererService {
|
|||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
// TODO: cache
|
||||||
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
|
name,
|
||||||
|
host: IsNull(),
|
||||||
|
});
|
||||||
|
|
||||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||||
}
|
}
|
||||||
@@ -472,7 +473,7 @@ export class ApRendererService {
|
|||||||
...hashtagTags,
|
...hashtagTags,
|
||||||
];
|
];
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const person = {
|
const person = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
@@ -639,7 +640,7 @@ export class ApRendererService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const ldSignature = this.ldSignatureService.use();
|
||||||
ldSignature.debug = false;
|
ldSignature.debug = false;
|
||||||
@@ -700,9 +701,13 @@ export class ApRendererService {
|
|||||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||||
if (names == null || names.length === 0) return [];
|
if (names == null || names.length === 0) return [];
|
||||||
|
|
||||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
const emojis = await Promise.all(
|
||||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
names.map(name => this.emojisRepository.findOneBy({
|
||||||
|
name,
|
||||||
|
host: IsNull(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
return emojis;
|
return emojis.filter(emoji => emoji != null) as Emoji[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -131,7 +131,7 @@ export class ApRequestService {
|
|||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairStoreService: UserKeypairStoreService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
@@ -143,7 +143,7 @@ export class ApRequestService {
|
|||||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedPost({
|
const req = ApRequestCreator.createSignedPost({
|
||||||
key: {
|
key: {
|
||||||
@@ -170,7 +170,7 @@ export class ApRequestService {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
public async signedGet(url: string, user: { id: User['id'] }) {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedGet({
|
const req = ApRequestCreator.createSignedGet({
|
||||||
key: {
|
key: {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { In } from 'typeorm';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
|
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@@ -342,17 +341,15 @@ export class ApNoteService {
|
|||||||
if (!tags) return [];
|
if (!tags) return [];
|
||||||
|
|
||||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
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 => {
|
return await Promise.all(eomjiTags.map(async tag => {
|
||||||
const name = tag.name!.replaceAll(':', '');
|
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
|
||||||
tag.icon = toSingle(tag.icon);
|
tag.icon = toSingle(tag.icon);
|
||||||
|
|
||||||
const exists = existingEmojis.find(x => x.name === name);
|
const exists = await this.emojisRepository.findOneBy({
|
||||||
|
host,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
if ((tag.updated != null && exists.updatedAt == null)
|
if ((tag.updated != null && exists.updatedAt == null)
|
||||||
|
@@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
|
|||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { User } from '@/models/entities/User.js';
|
import { User } from '@/models/entities/User.js';
|
||||||
import { truncate } from '@/misc/truncate.js';
|
import { truncate } from '@/misc/truncate.js';
|
||||||
import type { CacheService } from '@/core/CacheService.js';
|
import type { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
@@ -54,7 +54,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
private metaService: MetaService;
|
private metaService: MetaService;
|
||||||
private federatedInstanceService: FederatedInstanceService;
|
private federatedInstanceService: FederatedInstanceService;
|
||||||
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
||||||
private cacheService: CacheService;
|
private userCacheService: UserCacheService;
|
||||||
private apResolverService: ApResolverService;
|
private apResolverService: ApResolverService;
|
||||||
private apNoteService: ApNoteService;
|
private apNoteService: ApNoteService;
|
||||||
private apImageService: ApImageService;
|
private apImageService: ApImageService;
|
||||||
@@ -97,7 +97,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
//private metaService: MetaService,
|
//private metaService: MetaService,
|
||||||
//private federatedInstanceService: FederatedInstanceService,
|
//private federatedInstanceService: FederatedInstanceService,
|
||||||
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||||
//private cacheService: CacheService,
|
//private userCacheService: UserCacheService,
|
||||||
//private apResolverService: ApResolverService,
|
//private apResolverService: ApResolverService,
|
||||||
//private apNoteService: ApNoteService,
|
//private apNoteService: ApNoteService,
|
||||||
//private apImageService: ApImageService,
|
//private apImageService: ApImageService,
|
||||||
@@ -118,7 +118,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
this.metaService = this.moduleRef.get('MetaService');
|
this.metaService = this.moduleRef.get('MetaService');
|
||||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||||
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
||||||
this.cacheService = this.moduleRef.get('CacheService');
|
this.userCacheService = this.moduleRef.get('UserCacheService');
|
||||||
this.apResolverService = this.moduleRef.get('ApResolverService');
|
this.apResolverService = this.moduleRef.get('ApResolverService');
|
||||||
this.apNoteService = this.moduleRef.get('ApNoteService');
|
this.apNoteService = this.moduleRef.get('ApNoteService');
|
||||||
this.apImageService = this.moduleRef.get('ApImageService');
|
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> {
|
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
const cached = this.cacheService.uriPersonCache.get(uri);
|
const cached = this.userCacheService.uriPersonCache.get(uri);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(this.config.url + '/')) {
|
if (uri.startsWith(this.config.url + '/')) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const u = await this.usersRepository.findOneBy({ id });
|
const u = await this.usersRepository.findOneBy({ id });
|
||||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
if (u) this.userCacheService.uriPersonCache.set(uri, u);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
const exist = await this.usersRepository.findOneBy({ uri });
|
const exist = await this.usersRepository.findOneBy({ uri });
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.cacheService.uriPersonCache.set(uri, exist);
|
this.userCacheService.uriPersonCache.set(uri, exist);
|
||||||
return exist;
|
return exist;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository } from '@/models/index.js';
|
import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Antenna } from '@/models/entities/Antenna.js';
|
import type { Antenna } from '@/models/entities/Antenna.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -10,6 +10,9 @@ export class AntennaEntityService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
|
@Inject(DI.antennaNotesRepository)
|
||||||
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +22,8 @@ export class AntennaEntityService {
|
|||||||
): Promise<Packed<'Antenna'>> {
|
): Promise<Packed<'Antenna'>> {
|
||||||
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
|
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 {
|
return {
|
||||||
id: antenna.id,
|
id: antenna.id,
|
||||||
createdAt: antenna.createdAt.toISOString(),
|
createdAt: antenna.createdAt.toISOString(),
|
||||||
@@ -33,7 +38,7 @@ export class AntennaEntityService {
|
|||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
isActive: antenna.isActive,
|
isActive: antenna.isActive,
|
||||||
hasUnreadNote: false, // TODO
|
hasUnreadNote,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js';
|
import type { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/entities/Blocking.js';
|
import type { } from '@/models/entities/Blocking.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
import type { Channel } from '@/models/entities/Channel.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
import { NoteEntityService } from './NoteEntityService.js';
|
|
||||||
import { In } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChannelEntityService {
|
export class ChannelEntityService {
|
||||||
@@ -19,19 +18,13 @@ export class ChannelEntityService {
|
|||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.channelFavoritesRepository)
|
|
||||||
private channelFavoritesRepository: ChannelFavoritesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -40,7 +33,6 @@ export class ChannelEntityService {
|
|||||||
public async pack(
|
public async pack(
|
||||||
src: Channel['id'] | Channel,
|
src: Channel['id'] | Channel,
|
||||||
me?: { id: User['id'] } | null | undefined,
|
me?: { id: User['id'] } | null | undefined,
|
||||||
detailed?: boolean,
|
|
||||||
): Promise<Packed<'Channel'>> {
|
): Promise<Packed<'Channel'>> {
|
||||||
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
|
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
@@ -54,17 +46,6 @@ export class ChannelEntityService {
|
|||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
}) : null;
|
}) : 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 {
|
return {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
createdAt: channel.createdAt.toISOString(),
|
createdAt: channel.createdAt.toISOString(),
|
||||||
@@ -73,19 +54,13 @@ export class ChannelEntityService {
|
|||||||
description: channel.description,
|
description: channel.description,
|
||||||
userId: channel.userId,
|
userId: channel.userId,
|
||||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||||
pinnedNoteIds: channel.pinnedNoteIds,
|
|
||||||
usersCount: channel.usersCount,
|
usersCount: channel.usersCount,
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
|
|
||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing: following != null,
|
isFollowing: following != null,
|
||||||
isFavorited: favorite != null,
|
|
||||||
hasUnreadNote,
|
hasUnreadNote,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(detailed ? {
|
|
||||||
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
|
|
||||||
} : {}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -406,7 +406,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
|
||||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
||||||
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
|
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
|
||||||
@@ -420,30 +420,6 @@ 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
|
@bindThis
|
||||||
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
||||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { In } from 'typeorm';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AccessTokensRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
|
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
@@ -26,11 +25,8 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notificationsRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteReactionsRepository)
|
@Inject(DI.noteReactionsRepository)
|
||||||
private noteReactionsRepository: NoteReactionsRepository,
|
private noteReactionsRepository: NoteReactionsRepository,
|
||||||
@@ -52,40 +48,30 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Notification,
|
src: Notification['id'] | Notification,
|
||||||
meId: User['id'],
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
|
_hint_?: {
|
||||||
},
|
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
||||||
hint?: {
|
};
|
||||||
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
|
||||||
packedUsers: Map<User['id'], Packed<'User'>>;
|
|
||||||
},
|
},
|
||||||
): Promise<Packed<'Notification'>> {
|
): Promise<Packed<'Notification'>> {
|
||||||
const notification = src;
|
const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src });
|
||||||
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||||
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
||||||
hint?.packedNotes != null
|
options._hint_?.packedNotes != null
|
||||||
? hint.packedNotes.get(notification.noteId)
|
? options._hint_.packedNotes.get(notification.noteId)
|
||||||
: this.noteEntityService.pack(notification.noteId!, { id: meId }, {
|
: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : 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({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: notification.createdAt.toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
|
isRead: notification.isRead,
|
||||||
userId: notification.notifierId,
|
userId: notification.notifierId,
|
||||||
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
@@ -101,36 +87,33 @@ 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
|
@bindThis
|
||||||
public async packMany(
|
public async packMany(
|
||||||
notifications: Notification[],
|
notifications: Notification[],
|
||||||
meId: User['id'],
|
meId: User['id'],
|
||||||
) {
|
) {
|
||||||
if (notifications.length === 0) return [];
|
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 noteIds = notifications.map(x => x.noteId).filter(isNotNull);
|
const notes = notifications.map(x => x.note).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 }, {
|
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
|
return await Promise.all(notifications.map(x => this.pack(x, {
|
||||||
const users = userIds.length > 0 ? await this.usersRepository.find({
|
_hint_: {
|
||||||
where: { id: In(userIds) },
|
packedNotes,
|
||||||
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,
|
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, Not } from 'typeorm';
|
import { In, Not } from 'typeorm';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -9,11 +8,11 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||||
import { awaitAll } 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 { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
||||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
|
import 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 { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
@@ -53,7 +52,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
private customEmojiService: CustomEmojiService;
|
private customEmojiService: CustomEmojiService;
|
||||||
private antennaService: AntennaService;
|
private antennaService: AntennaService;
|
||||||
private roleService: RoleService;
|
private roleService: RoleService;
|
||||||
private userInstanceCache: MemoryKVCache<Instance | null>;
|
private userInstanceCache: KVCache<Instance | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
@@ -61,9 +60,6 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -94,6 +90,9 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notificationsRepository)
|
||||||
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
@Inject(DI.userNotePiningsRepository)
|
@Inject(DI.userNotePiningsRepository)
|
||||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||||
|
|
||||||
@@ -109,6 +108,9 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
@Inject(DI.announcementsRepository)
|
@Inject(DI.announcementsRepository)
|
||||||
private announcementsRepository: AnnouncementsRepository,
|
private announcementsRepository: AnnouncementsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.antennaNotesRepository)
|
||||||
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.pagesRepository)
|
@Inject(DI.pagesRepository)
|
||||||
private pagesRepository: PagesRepository,
|
private pagesRepository: PagesRepository,
|
||||||
|
|
||||||
@@ -119,7 +121,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
//private antennaService: AntennaService,
|
//private antennaService: AntennaService,
|
||||||
//private roleService: RoleService,
|
//private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
this.userInstanceCache = new MemoryKVCache<Instance | null>(1000 * 60 * 60 * 3);
|
this.userInstanceCache = new KVCache<Instance | null>(1000 * 60 * 60 * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
@@ -221,7 +223,6 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||||
/*
|
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
||||||
|
|
||||||
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
||||||
@@ -230,22 +231,37 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
return unread != null;
|
return unread != null;
|
||||||
*/
|
}
|
||||||
return false; // TODO
|
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
const mute = await this.mutingsRepository.findBy({
|
||||||
|
muterId: userId,
|
||||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
});
|
||||||
`notificationTimeline:${userId}`,
|
const mutedUserIds = mute.map(m => m.muteeId);
|
||||||
'+',
|
|
||||||
'-',
|
|
||||||
'COUNT', 1);
|
|
||||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
|
||||||
|
|
||||||
return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
|
const count = await this.notificationsRepository.count({
|
||||||
|
where: {
|
||||||
|
notifieeId: userId,
|
||||||
|
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -451,7 +467,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
}).then(count => count > 0),
|
}).then(count => count > 0),
|
||||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||||
hasUnreadChannel: false, // 後方互換性のため
|
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
|
@@ -33,6 +33,7 @@ export const DI = {
|
|||||||
emojisRepository: Symbol('emojisRepository'),
|
emojisRepository: Symbol('emojisRepository'),
|
||||||
driveFilesRepository: Symbol('driveFilesRepository'),
|
driveFilesRepository: Symbol('driveFilesRepository'),
|
||||||
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
||||||
|
notificationsRepository: Symbol('notificationsRepository'),
|
||||||
metasRepository: Symbol('metasRepository'),
|
metasRepository: Symbol('metasRepository'),
|
||||||
mutingsRepository: Symbol('mutingsRepository'),
|
mutingsRepository: Symbol('mutingsRepository'),
|
||||||
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
|
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
|
||||||
@@ -53,13 +54,14 @@ export const DI = {
|
|||||||
clipNotesRepository: Symbol('clipNotesRepository'),
|
clipNotesRepository: Symbol('clipNotesRepository'),
|
||||||
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
|
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
|
||||||
antennasRepository: Symbol('antennasRepository'),
|
antennasRepository: Symbol('antennasRepository'),
|
||||||
|
antennaNotesRepository: Symbol('antennaNotesRepository'),
|
||||||
promoNotesRepository: Symbol('promoNotesRepository'),
|
promoNotesRepository: Symbol('promoNotesRepository'),
|
||||||
promoReadsRepository: Symbol('promoReadsRepository'),
|
promoReadsRepository: Symbol('promoReadsRepository'),
|
||||||
relaysRepository: Symbol('relaysRepository'),
|
relaysRepository: Symbol('relaysRepository'),
|
||||||
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
||||||
channelsRepository: Symbol('channelsRepository'),
|
channelsRepository: Symbol('channelsRepository'),
|
||||||
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
||||||
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
channelNotePiningsRepository: Symbol('channelNotePiningsRepository'),
|
||||||
registryItemsRepository: Symbol('registryItemsRepository'),
|
registryItemsRepository: Symbol('registryItemsRepository'),
|
||||||
webhooksRepository: Symbol('webhooksRepository'),
|
webhooksRepository: Symbol('webhooksRepository'),
|
||||||
adsRepository: Symbol('adsRepository'),
|
adsRepository: Symbol('adsRepository'),
|
||||||
|
@@ -1,187 +1,18 @@
|
|||||||
import Redis from 'ioredis';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
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: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||||
|
|
||||||
export class MemoryKVCache<T> {
|
export class KVCache<T> {
|
||||||
public cache: Map<string, { date: number; value: T; }>;
|
public cache: Map<string | null, { date: number; value: T; }>;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
|
|
||||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
constructor(lifetime: KVCache<never>['lifetime']) {
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public set(key: string, value: T): void {
|
public set(key: string | null, value: T): void {
|
||||||
this.cache.set(key, {
|
this.cache.set(key, {
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
value,
|
value,
|
||||||
@@ -189,7 +20,7 @@ export class MemoryKVCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public get(key: string): T | undefined {
|
public get(key: string | null): T | undefined {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
if ((Date.now() - cached.date) > this.lifetime) {
|
if ((Date.now() - cached.date) > this.lifetime) {
|
||||||
@@ -200,7 +31,7 @@ export class MemoryKVCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public delete(key: string) {
|
public delete(key: string | null) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +40,7 @@ export class MemoryKVCache<T> {
|
|||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||||
const cachedValue = this.get(key);
|
const cachedValue = this.get(key);
|
||||||
if (cachedValue !== undefined) {
|
if (cachedValue !== undefined) {
|
||||||
if (validator) {
|
if (validator) {
|
||||||
@@ -234,7 +65,7 @@ export class MemoryKVCache<T> {
|
|||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||||
const cachedValue = this.get(key);
|
const cachedValue = this.get(key);
|
||||||
if (cachedValue !== undefined) {
|
if (cachedValue !== undefined) {
|
||||||
if (validator) {
|
if (validator) {
|
||||||
@@ -257,12 +88,12 @@ export class MemoryKVCache<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemorySingleCache<T> {
|
export class Cache<T> {
|
||||||
private cachedAt: number | null = null;
|
private cachedAt: number | null = null;
|
||||||
private value: T | undefined;
|
private value: T | undefined;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
|
|
||||||
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
|
constructor(lifetime: Cache<never>['lifetime']) {
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,8 +23,3 @@ export function genAid(date: Date): string {
|
|||||||
counter++;
|
counter++;
|
||||||
return getTime(t) + getNoise();
|
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) };
|
|
||||||
}
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.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 { 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 type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
@@ -172,6 +172,12 @@ const $driveFoldersRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $notificationsRepository: Provider = {
|
||||||
|
provide: DI.notificationsRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(Notification),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $metasRepository: Provider = {
|
const $metasRepository: Provider = {
|
||||||
provide: DI.metasRepository,
|
provide: DI.metasRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(Meta),
|
useFactory: (db: DataSource) => db.getRepository(Meta),
|
||||||
@@ -292,6 +298,12 @@ const $antennasRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $antennaNotesRepository: Provider = {
|
||||||
|
provide: DI.antennaNotesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(AntennaNote),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $promoNotesRepository: Provider = {
|
const $promoNotesRepository: Provider = {
|
||||||
provide: DI.promoNotesRepository,
|
provide: DI.promoNotesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(PromoNote),
|
useFactory: (db: DataSource) => db.getRepository(PromoNote),
|
||||||
@@ -328,9 +340,9 @@ const $channelFollowingsRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $channelFavoritesRepository: Provider = {
|
const $channelNotePiningsRepository: Provider = {
|
||||||
provide: DI.channelFavoritesRepository,
|
provide: DI.channelNotePiningsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(ChannelFavorite),
|
useFactory: (db: DataSource) => db.getRepository(ChannelNotePining),
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -420,6 +432,7 @@ const $roleAssignmentsRepository: Provider = {
|
|||||||
$emojisRepository,
|
$emojisRepository,
|
||||||
$driveFilesRepository,
|
$driveFilesRepository,
|
||||||
$driveFoldersRepository,
|
$driveFoldersRepository,
|
||||||
|
$notificationsRepository,
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
$renoteMutingsRepository,
|
$renoteMutingsRepository,
|
||||||
@@ -440,13 +453,14 @@ const $roleAssignmentsRepository: Provider = {
|
|||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
$clipFavoritesRepository,
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
|
$antennaNotesRepository,
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
$mutedNotesRepository,
|
$mutedNotesRepository,
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
$channelFavoritesRepository,
|
$channelNotePiningsRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
$adsRepository,
|
$adsRepository,
|
||||||
@@ -486,6 +500,7 @@ const $roleAssignmentsRepository: Provider = {
|
|||||||
$emojisRepository,
|
$emojisRepository,
|
||||||
$driveFilesRepository,
|
$driveFilesRepository,
|
||||||
$driveFoldersRepository,
|
$driveFoldersRepository,
|
||||||
|
$notificationsRepository,
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
$renoteMutingsRepository,
|
$renoteMutingsRepository,
|
||||||
@@ -506,13 +521,14 @@ const $roleAssignmentsRepository: Provider = {
|
|||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
$clipFavoritesRepository,
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
|
$antennaNotesRepository,
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
$mutedNotesRepository,
|
$mutedNotesRepository,
|
||||||
$channelsRepository,
|
$channelsRepository,
|
||||||
$channelFollowingsRepository,
|
$channelFollowingsRepository,
|
||||||
$channelFavoritesRepository,
|
$channelNotePiningsRepository,
|
||||||
$registryItemsRepository,
|
$registryItemsRepository,
|
||||||
$webhooksRepository,
|
$webhooksRepository,
|
||||||
$adsRepository,
|
$adsRepository,
|
||||||
|
43
packages/backend/src/models/entities/AntennaNote.ts
Normal file
43
packages/backend/src/models/entities/AntennaNote.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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;
|
||||||
|
}
|
@@ -59,11 +59,6 @@ export class Channel {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public banner: DriveFile | null;
|
public banner: DriveFile | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
array: true, length: 128, default: '{}',
|
|
||||||
})
|
|
||||||
public pinnedNoteIds: string[];
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@@ -1,24 +1,21 @@
|
|||||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
import { User } from './User.js';
|
import { Note } from './Note.js';
|
||||||
import { Channel } from './Channel.js';
|
import { Channel } from './Channel.js';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['userId', 'channelId'], { unique: true })
|
@Index(['channelId', 'noteId'], { unique: true })
|
||||||
export class ChannelFavorite {
|
export class ChannelNotePining {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
comment: 'The created date of the ChannelFavorite.',
|
comment: 'The created date of the ChannelNotePining.',
|
||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column(id())
|
||||||
...id(),
|
|
||||||
})
|
|
||||||
public channelId: Channel['id'];
|
public channelId: Channel['id'];
|
||||||
|
|
||||||
@ManyToOne(type => Channel, {
|
@ManyToOne(type => Channel, {
|
||||||
@@ -27,15 +24,12 @@ export class ChannelFavorite {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public channel: Channel | null;
|
public channel: Channel | null;
|
||||||
|
|
||||||
@Index()
|
@Column(id())
|
||||||
@Column({
|
public noteId: Note['id'];
|
||||||
...id(),
|
|
||||||
})
|
|
||||||
public userId: User['id'];
|
|
||||||
|
|
||||||
@ManyToOne(type => User, {
|
@ManyToOne(type => Note, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public user: User | null;
|
public note: Note | null;
|
||||||
}
|
}
|
@@ -1,19 +1,54 @@
|
|||||||
import { notificationTypes } from '@/types.js';
|
import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
|
||||||
|
import { notificationTypes, obsoleteNotificationTypes } from '@/types.js';
|
||||||
|
import { id } from '../id.js';
|
||||||
import { User } from './User.js';
|
import { User } from './User.js';
|
||||||
import { Note } from './Note.js';
|
import { Note } from './Note.js';
|
||||||
import { FollowRequest } from './FollowRequest.js';
|
import { FollowRequest } from './FollowRequest.js';
|
||||||
import { AccessToken } from './AccessToken.js';
|
import { AccessToken } from './AccessToken.js';
|
||||||
|
|
||||||
export type Notification = {
|
@Entity()
|
||||||
id: string;
|
export class Notification {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
// RedisのためDateではなくstring
|
@Index()
|
||||||
createdAt: string;
|
@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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の送信者(initiator)
|
* 通知の送信者(initiator)
|
||||||
*/
|
*/
|
||||||
notifierId: User['id'] | null;
|
@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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の種類。
|
* 通知の種類。
|
||||||
@@ -29,37 +64,104 @@ export type Notification = {
|
|||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
*/
|
*/
|
||||||
type: typeof notificationTypes[number];
|
@Index()
|
||||||
|
@Column('enum', {
|
||||||
|
enum: [
|
||||||
|
...notificationTypes,
|
||||||
|
...obsoleteNotificationTypes,
|
||||||
|
],
|
||||||
|
comment: 'The type of the Notification.',
|
||||||
|
})
|
||||||
|
public type: typeof notificationTypes[number];
|
||||||
|
|
||||||
noteId: Note['id'] | null;
|
/**
|
||||||
|
* 通知が読まれたかどうか
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
comment: 'Whether the Notification is read.',
|
||||||
|
})
|
||||||
|
public isRead: boolean;
|
||||||
|
|
||||||
followRequestId: FollowRequest['id'] | null;
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public noteId: Note['id'] | null;
|
||||||
|
|
||||||
reaction: string | null;
|
@ManyToOne(type => Note, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public note: Note | null;
|
||||||
|
|
||||||
choice: number | null;
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public followRequestId: FollowRequest['id'] | null;
|
||||||
|
|
||||||
achievement: string | 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のbody
|
* アプリ通知のbody
|
||||||
*/
|
*/
|
||||||
customBody: string | null;
|
@Column('varchar', {
|
||||||
|
length: 2048, nullable: true,
|
||||||
|
})
|
||||||
|
public customBody: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のheader
|
* アプリ通知のheader
|
||||||
* (省略時はアプリ名で表示されることを期待)
|
* (省略時はアプリ名で表示されることを期待)
|
||||||
*/
|
*/
|
||||||
customHeader: string | null;
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public customHeader: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のicon(URL)
|
* アプリ通知のicon(URL)
|
||||||
* (省略時はアプリアイコンで表示されることを期待)
|
* (省略時はアプリアイコンで表示されることを期待)
|
||||||
*/
|
*/
|
||||||
customIcon: string | null;
|
@Column('varchar', {
|
||||||
|
length: 1024, nullable: true,
|
||||||
|
})
|
||||||
|
public customIcon: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のアプリ(のトークン)
|
* アプリ通知のアプリ(のトークン)
|
||||||
*/
|
*/
|
||||||
appAccessTokenId: AccessToken['id'] | null;
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public appAccessTokenId: AccessToken['id'] | null;
|
||||||
|
|
||||||
|
@ManyToOne(type => AccessToken, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public appAccessToken: AccessToken | null;
|
||||||
}
|
}
|
||||||
|
@@ -4,12 +4,13 @@ import { Ad } from '@/models/entities/Ad.js';
|
|||||||
import { Announcement } from '@/models/entities/Announcement.js';
|
import { Announcement } from '@/models/entities/Announcement.js';
|
||||||
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
||||||
import { Antenna } from '@/models/entities/Antenna.js';
|
import { Antenna } from '@/models/entities/Antenna.js';
|
||||||
|
import { AntennaNote } from '@/models/entities/AntennaNote.js';
|
||||||
import { App } from '@/models/entities/App.js';
|
import { App } from '@/models/entities/App.js';
|
||||||
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
import { Blocking } from '@/models/entities/Blocking.js';
|
import { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
||||||
@@ -32,6 +33,7 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
|||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
||||||
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
||||||
|
import { Notification } from '@/models/entities/Notification.js';
|
||||||
import { Page } from '@/models/entities/Page.js';
|
import { Page } from '@/models/entities/Page.js';
|
||||||
import { PageLike } from '@/models/entities/PageLike.js';
|
import { PageLike } from '@/models/entities/PageLike.js';
|
||||||
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
||||||
@@ -71,12 +73,13 @@ export {
|
|||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementRead,
|
AnnouncementRead,
|
||||||
Antenna,
|
Antenna,
|
||||||
|
AntennaNote,
|
||||||
App,
|
App,
|
||||||
AttestationChallenge,
|
AttestationChallenge,
|
||||||
AuthSession,
|
AuthSession,
|
||||||
Blocking,
|
Blocking,
|
||||||
ChannelFollowing,
|
ChannelFollowing,
|
||||||
ChannelFavorite,
|
ChannelNotePining,
|
||||||
Clip,
|
Clip,
|
||||||
ClipNote,
|
ClipNote,
|
||||||
ClipFavorite,
|
ClipFavorite,
|
||||||
@@ -99,6 +102,7 @@ export {
|
|||||||
NoteReaction,
|
NoteReaction,
|
||||||
NoteThreadMuting,
|
NoteThreadMuting,
|
||||||
NoteUnread,
|
NoteUnread,
|
||||||
|
Notification,
|
||||||
Page,
|
Page,
|
||||||
PageLike,
|
PageLike,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
@@ -137,12 +141,13 @@ export type AdsRepository = Repository<Ad>;
|
|||||||
export type AnnouncementsRepository = Repository<Announcement>;
|
export type AnnouncementsRepository = Repository<Announcement>;
|
||||||
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
|
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
|
||||||
export type AntennasRepository = Repository<Antenna>;
|
export type AntennasRepository = Repository<Antenna>;
|
||||||
|
export type AntennaNotesRepository = Repository<AntennaNote>;
|
||||||
export type AppsRepository = Repository<App>;
|
export type AppsRepository = Repository<App>;
|
||||||
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
|
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
|
||||||
export type AuthSessionsRepository = Repository<AuthSession>;
|
export type AuthSessionsRepository = Repository<AuthSession>;
|
||||||
export type BlockingsRepository = Repository<Blocking>;
|
export type BlockingsRepository = Repository<Blocking>;
|
||||||
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
|
export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
|
||||||
export type ChannelFavoritesRepository = Repository<ChannelFavorite>;
|
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
|
||||||
export type ClipsRepository = Repository<Clip>;
|
export type ClipsRepository = Repository<Clip>;
|
||||||
export type ClipNotesRepository = Repository<ClipNote>;
|
export type ClipNotesRepository = Repository<ClipNote>;
|
||||||
export type ClipFavoritesRepository = Repository<ClipFavorite>;
|
export type ClipFavoritesRepository = Repository<ClipFavorite>;
|
||||||
@@ -165,6 +170,7 @@ export type NoteFavoritesRepository = Repository<NoteFavorite>;
|
|||||||
export type NoteReactionsRepository = Repository<NoteReaction>;
|
export type NoteReactionsRepository = Repository<NoteReaction>;
|
||||||
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
|
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
|
||||||
export type NoteUnreadsRepository = Repository<NoteUnread>;
|
export type NoteUnreadsRepository = Repository<NoteUnread>;
|
||||||
|
export type NotificationsRepository = Repository<Notification>;
|
||||||
export type PagesRepository = Repository<Page>;
|
export type PagesRepository = Repository<Page>;
|
||||||
export type PageLikesRepository = Repository<PageLike>;
|
export type PageLikesRepository = Repository<PageLike>;
|
||||||
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;
|
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;
|
||||||
|
@@ -42,22 +42,10 @@ export const packedChannelSchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
isFavorited: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
pinnedNoteIds: {
|
|
||||||
type: 'array',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
format: 'id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -14,6 +14,10 @@ export const packedNotificationSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
isRead: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@@ -311,6 +311,10 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
hasUnreadChannel: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
hasUnreadNotification: {
|
hasUnreadNotification: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@@ -12,12 +12,13 @@ import { Ad } from '@/models/entities/Ad.js';
|
|||||||
import { Announcement } from '@/models/entities/Announcement.js';
|
import { Announcement } from '@/models/entities/Announcement.js';
|
||||||
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
||||||
import { Antenna } from '@/models/entities/Antenna.js';
|
import { Antenna } from '@/models/entities/Antenna.js';
|
||||||
|
import { AntennaNote } from '@/models/entities/AntennaNote.js';
|
||||||
import { App } from '@/models/entities/App.js';
|
import { App } from '@/models/entities/App.js';
|
||||||
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
import { Blocking } from '@/models/entities/Blocking.js';
|
import { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
||||||
@@ -40,6 +41,7 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
|||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
||||||
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
||||||
|
import { Notification } from '@/models/entities/Notification.js';
|
||||||
import { Page } from '@/models/entities/Page.js';
|
import { Page } from '@/models/entities/Page.js';
|
||||||
import { PageLike } from '@/models/entities/PageLike.js';
|
import { PageLike } from '@/models/entities/PageLike.js';
|
||||||
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
||||||
@@ -154,6 +156,7 @@ export const entities = [
|
|||||||
DriveFolder,
|
DriveFolder,
|
||||||
Poll,
|
Poll,
|
||||||
PollVote,
|
PollVote,
|
||||||
|
Notification,
|
||||||
Emoji,
|
Emoji,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
SwSubscription,
|
SwSubscription,
|
||||||
@@ -165,13 +168,14 @@ export const entities = [
|
|||||||
ClipNote,
|
ClipNote,
|
||||||
ClipFavorite,
|
ClipFavorite,
|
||||||
Antenna,
|
Antenna,
|
||||||
|
AntennaNote,
|
||||||
PromoNote,
|
PromoNote,
|
||||||
PromoRead,
|
PromoRead,
|
||||||
Relay,
|
Relay,
|
||||||
MutedNote,
|
MutedNote,
|
||||||
Channel,
|
Channel,
|
||||||
ChannelFollowing,
|
ChannelFollowing,
|
||||||
ChannelFavorite,
|
ChannelNotePining,
|
||||||
RegistryItem,
|
RegistryItem,
|
||||||
Ad,
|
Ad,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
|
@@ -4,10 +4,10 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import type { MutingsRepository } from '@/models/index.js';
|
import type { MutingsRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CheckExpiredMutingsProcessorService {
|
export class CheckExpiredMutingsProcessorService {
|
||||||
@@ -20,7 +20,7 @@ export class CheckExpiredMutingsProcessorService {
|
|||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private userMutingService: UserMutingService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
||||||
@@ -37,7 +37,13 @@ export class CheckExpiredMutingsProcessorService {
|
|||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (expired.length > 0) {
|
if (expired.length > 0) {
|
||||||
await this.userMutingService.unmute(expired);
|
await this.mutingsRepository.delete({
|
||||||
|
id: In(expired.map(m => m.id)),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const m of expired) {
|
||||||
|
this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All expired mutings checked.');
|
this.logger.succ('All expired mutings checked.');
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
|
import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -20,12 +20,18 @@ export class CleanProcessorService {
|
|||||||
@Inject(DI.userIpsRepository)
|
@Inject(DI.userIpsRepository)
|
||||||
private userIpsRepository: UserIpsRepository,
|
private userIpsRepository: UserIpsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notificationsRepository)
|
||||||
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
@Inject(DI.mutedNotesRepository)
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
private mutedNotesRepository: MutedNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
|
@Inject(DI.antennaNotesRepository)
|
||||||
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.roleAssignmentsRepository)
|
@Inject(DI.roleAssignmentsRepository)
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
@@ -43,6 +49,10 @@ export class CleanProcessorService {
|
|||||||
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
|
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({
|
this.mutedNotesRepository.delete({
|
||||||
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
||||||
reason: 'word',
|
reason: 'word',
|
||||||
|
@@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
@@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeliverProcessorService {
|
export class DeliverProcessorService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private suspendedHostsCache: MemorySingleCache<Instance[]>;
|
private suspendedHostsCache: KVCache<Instance[]>;
|
||||||
private latest: string | null;
|
private latest: string | null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -46,7 +46,7 @@ export class DeliverProcessorService {
|
|||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
||||||
this.suspendedHostsCache = new MemorySingleCache<Instance[]>(1000 * 60 * 60);
|
this.suspendedHostsCache = new KVCache<Instance[]>(1000 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -60,14 +60,14 @@ export class DeliverProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isSuspendedなら中断
|
// isSuspendedなら中断
|
||||||
let suspendedHosts = this.suspendedHostsCache.get();
|
let suspendedHosts = this.suspendedHostsCache.get(null);
|
||||||
if (suspendedHosts == null) {
|
if (suspendedHosts == null) {
|
||||||
suspendedHosts = await this.instancesRepository.find({
|
suspendedHosts = await this.instancesRepository.find({
|
||||||
where: {
|
where: {
|
||||||
isSuspended: true,
|
isSuspended: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.suspendedHostsCache.set(suspendedHosts);
|
this.suspendedHostsCache.set(null, suspendedHosts);
|
||||||
}
|
}
|
||||||
if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) {
|
if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) {
|
||||||
return 'skip (suspended)';
|
return 'skip (suspended)';
|
||||||
|
@@ -86,10 +86,6 @@ export class ImportCustomEmojisProcessorService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const emojiInfo = record.emoji;
|
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;
|
const emojiPath = outputPath + '/' + record.fileName;
|
||||||
await this.emojisRepository.delete({
|
await this.emojisRepository.delete({
|
||||||
name: emojiInfo.name,
|
name: emojiInfo.name,
|
||||||
|
@@ -39,7 +39,6 @@ export class WebhookDeliverProcessorService {
|
|||||||
'X-Misskey-Host': this.config.host,
|
'X-Misskey-Host': this.config.host,
|
||||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||||
'X-Misskey-Hook-Secret': job.data.secret,
|
'X-Misskey-Hook-Secret': job.data.secret,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
hookId: job.data.webhookId,
|
hookId: job.data.webhookId,
|
||||||
|
@@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
|||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||||
import type { Following } from '@/models/entities/Following.js';
|
import type { Following } from '@/models/entities/Following.js';
|
||||||
import { countIf } from '@/misc/prelude/array.js';
|
import { countIf } from '@/misc/prelude/array.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
@@ -58,7 +58,7 @@ export class ActivityPubServerService {
|
|||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairStoreService: UserKeypairStoreService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
@@ -540,7 +540,7 @@ export class ActivityPubServerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
|
@@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import NotesChart from '@/core/chart/charts/notes.js';
|
import NotesChart from '@/core/chart/charts/notes.js';
|
||||||
@@ -118,17 +118,17 @@ export class NodeinfoServerService {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
const cache = new KVCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||||
|
|
||||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2());
|
const base = await cache.fetch(null, () => nodeinfo2());
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=600');
|
reply.header('Cache-Control', 'public, max-age=600');
|
||||||
return { version: '2.1', ...base };
|
return { version: '2.1', ...base };
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2());
|
const base = await cache.fetch(null, () => nodeinfo2());
|
||||||
|
|
||||||
delete (base as any).software.repository;
|
delete (base as any).software.repository;
|
||||||
|
|
||||||
|
@@ -3,9 +3,9 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { KVCache } from '@/misc/cache.js';
|
||||||
import type { App } from '@/models/entities/App.js';
|
import type { App } from '@/models/entities/App.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
import isNativeToken from '@/misc/is-native-token.js';
|
import isNativeToken from '@/misc/is-native-token.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export class AuthenticationError extends Error {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthenticateService {
|
export class AuthenticateService {
|
||||||
private appCache: MemoryKVCache<App>;
|
private appCache: KVCache<App>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
@@ -30,9 +30,9 @@ export class AuthenticateService {
|
|||||||
@Inject(DI.appsRepository)
|
@Inject(DI.appsRepository)
|
||||||
private appsRepository: AppsRepository,
|
private appsRepository: AppsRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private userCacheService: UserCacheService,
|
||||||
) {
|
) {
|
||||||
this.appCache = new MemoryKVCache<App>(Infinity);
|
this.appCache = new KVCache<App>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -42,7 +42,7 @@ export class AuthenticateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNativeToken(token)) {
|
if (isNativeToken(token)) {
|
||||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
|
const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token,
|
||||||
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
@@ -67,7 +67,7 @@ export class AuthenticateService {
|
|||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
|
const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId,
|
||||||
() => this.usersRepository.findOneBy({
|
() => this.usersRepository.findOneBy({
|
||||||
id: accessToken.userId,
|
id: accessToken.userId,
|
||||||
}) as Promise<LocalUser>);
|
}) as Promise<LocalUser>);
|
||||||
|
@@ -95,9 +95,6 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
|||||||
import * as ep___channels_timeline from './endpoints/channels/timeline.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_unfollow from './endpoints/channels/unfollow.js';
|
||||||
import * as ep___channels_update from './endpoints/channels/update.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_activeUsers from './endpoints/charts/active-users.js';
|
||||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||||
@@ -268,6 +265,7 @@ 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___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.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_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___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||||
@@ -426,9 +424,6 @@ 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_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_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_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_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_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
||||||
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
||||||
@@ -599,6 +594,7 @@ 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 $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_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_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 $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_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
||||||
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
||||||
@@ -761,9 +757,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$channels_timeline,
|
$channels_timeline,
|
||||||
$channels_unfollow,
|
$channels_unfollow,
|
||||||
$channels_update,
|
$channels_update,
|
||||||
$channels_favorite,
|
|
||||||
$channels_unfavorite,
|
|
||||||
$channels_myFavorites,
|
|
||||||
$charts_activeUsers,
|
$charts_activeUsers,
|
||||||
$charts_apRequest,
|
$charts_apRequest,
|
||||||
$charts_drive,
|
$charts_drive,
|
||||||
@@ -934,6 +927,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$notes_userListTimeline,
|
$notes_userListTimeline,
|
||||||
$notifications_create,
|
$notifications_create,
|
||||||
$notifications_markAllAsRead,
|
$notifications_markAllAsRead,
|
||||||
|
$notifications_read,
|
||||||
$pagePush,
|
$pagePush,
|
||||||
$pages_create,
|
$pages_create,
|
||||||
$pages_delete,
|
$pages_delete,
|
||||||
@@ -1090,9 +1084,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$channels_timeline,
|
$channels_timeline,
|
||||||
$channels_unfollow,
|
$channels_unfollow,
|
||||||
$channels_update,
|
$channels_update,
|
||||||
$channels_favorite,
|
|
||||||
$channels_unfavorite,
|
|
||||||
$channels_myFavorites,
|
|
||||||
$charts_activeUsers,
|
$charts_activeUsers,
|
||||||
$charts_apRequest,
|
$charts_apRequest,
|
||||||
$charts_drive,
|
$charts_drive,
|
||||||
@@ -1263,6 +1254,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||||||
$notes_userListTimeline,
|
$notes_userListTimeline,
|
||||||
$notifications_create,
|
$notifications_create,
|
||||||
$notifications_markAllAsRead,
|
$notifications_markAllAsRead,
|
||||||
|
$notifications_read,
|
||||||
$pagePush,
|
$pagePush,
|
||||||
$pages_create,
|
$pages_create,
|
||||||
$pages_delete,
|
$pages_delete,
|
||||||
|
@@ -9,7 +9,6 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
|||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
|
||||||
import { AuthenticateService } from './AuthenticateService.js';
|
import { AuthenticateService } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/index.js';
|
import MainStreamConnection from './stream/index.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
import { ChannelsService } from './stream/ChannelsService.js';
|
||||||
@@ -46,7 +45,7 @@ export class StreamingApiServerService {
|
|||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private globalEventService: GlobalEventService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private channelsService: ChannelsService,
|
private channelsService: ChannelsService,
|
||||||
@@ -74,6 +73,8 @@ export class StreamingApiServerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connection = request.accept();
|
||||||
|
|
||||||
const ev = new EventEmitter();
|
const ev = new EventEmitter();
|
||||||
|
|
||||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||||
@@ -84,19 +85,19 @@ export class StreamingApiServerService {
|
|||||||
this.redisSubscriber.on('message', onRedisMessage);
|
this.redisSubscriber.on('message', onRedisMessage);
|
||||||
|
|
||||||
const main = new MainStreamConnection(
|
const main = new MainStreamConnection(
|
||||||
|
this.followingsRepository,
|
||||||
|
this.mutingsRepository,
|
||||||
|
this.renoteMutingsRepository,
|
||||||
|
this.blockingsRepository,
|
||||||
|
this.channelFollowingsRepository,
|
||||||
|
this.userProfilesRepository,
|
||||||
this.channelsService,
|
this.channelsService,
|
||||||
|
this.globalEventService,
|
||||||
this.noteReadService,
|
this.noteReadService,
|
||||||
this.notificationService,
|
this.notificationService,
|
||||||
this.cacheService,
|
connection, ev, user, miapp,
|
||||||
ev, user, miapp,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await main.init();
|
|
||||||
|
|
||||||
const connection = request.accept();
|
|
||||||
|
|
||||||
main.init2(connection);
|
|
||||||
|
|
||||||
const intervalId = user ? setInterval(() => {
|
const intervalId = user ? setInterval(() => {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersRepository.update(user.id, {
|
||||||
lastActiveDate: new Date(),
|
lastActiveDate: new Date(),
|
||||||
|
@@ -95,9 +95,6 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
|||||||
import * as ep___channels_timeline from './endpoints/channels/timeline.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_unfollow from './endpoints/channels/unfollow.js';
|
||||||
import * as ep___channels_update from './endpoints/channels/update.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_activeUsers from './endpoints/charts/active-users.js';
|
||||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||||
@@ -268,6 +265,7 @@ 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___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.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_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___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||||
@@ -424,9 +422,6 @@ const eps = [
|
|||||||
['channels/timeline', ep___channels_timeline],
|
['channels/timeline', ep___channels_timeline],
|
||||||
['channels/unfollow', ep___channels_unfollow],
|
['channels/unfollow', ep___channels_unfollow],
|
||||||
['channels/update', ep___channels_update],
|
['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/active-users', ep___charts_activeUsers],
|
||||||
['charts/ap-request', ep___charts_apRequest],
|
['charts/ap-request', ep___charts_apRequest],
|
||||||
['charts/drive', ep___charts_drive],
|
['charts/drive', ep___charts_drive],
|
||||||
@@ -597,6 +592,7 @@ const eps = [
|
|||||||
['notes/user-list-timeline', ep___notes_userListTimeline],
|
['notes/user-list-timeline', ep___notes_userListTimeline],
|
||||||
['notifications/create', ep___notifications_create],
|
['notifications/create', ep___notifications_create],
|
||||||
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
||||||
|
['notifications/read', ep___notifications_read],
|
||||||
['page-push', ep___pagePush],
|
['page-push', ep___pagePush],
|
||||||
['pages/create', ep___pages_create],
|
['pages/create', ep___pages_create],
|
||||||
['pages/delete', ep___pages_delete],
|
['pages/delete', ep___pages_delete],
|
||||||
|
@@ -61,6 +61,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
|
// Terminate streaming
|
||||||
|
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -22,14 +26,38 @@ export const paramDef = {
|
|||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
|
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),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -90,6 +90,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await this.db.queryResultCache?.remove(['meta_emojis']);
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService';
|
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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -19,14 +24,38 @@ export const paramDef = {
|
|||||||
required: ['ids'],
|
required: ['ids'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.deleteBulk(ps.ids);
|
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),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -25,14 +31,38 @@ export const paramDef = {
|
|||||||
required: ['id'],
|
required: ['id'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.delete(ps.id);
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -22,14 +26,38 @@ export const paramDef = {
|
|||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
|
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),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -22,14 +26,34 @@ export const paramDef = {
|
|||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
|
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),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -24,14 +28,34 @@ export const paramDef = {
|
|||||||
required: ['ids'],
|
required: ['ids'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
|
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),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource, IsNull } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.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 { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -41,19 +45,51 @@ export const paramDef = {
|
|||||||
required: ['id', 'name', 'aliases'],
|
required: ['id', 'name', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private customEmojiService: CustomEmojiService,
|
@Inject(DI.db)
|
||||||
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.emojisRepository)
|
||||||
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.customEmojiService.update(ps.id, {
|
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(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
category: ps.category ?? null,
|
category: ps.category,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
license: ps.license ?? null,
|
license: ps.license,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
|
import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
@@ -36,6 +36,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notificationsRepository)
|
||||||
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private userSuspendService: UserSuspendService,
|
private userSuspendService: UserSuspendService,
|
||||||
@@ -62,9 +65,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Terminate streaming
|
||||||
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
|
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||||
await this.unFollowAll(user).catch(e => {});
|
await this.unFollowAll(user).catch(e => {});
|
||||||
|
await this.readAllNotify(user).catch(e => {});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -87,4 +96,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
await this.userFollowingService.unfollow(follower, followee, true);
|
await this.userFollowingService.unfollow(follower, followee, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async readAllNotify(notifier: User) {
|
||||||
|
await this.notificationsRepository.update({
|
||||||
|
notifierId: notifier.id,
|
||||||
|
isRead: false,
|
||||||
|
}, {
|
||||||
|
isRead: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
|
import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -52,16 +50,15 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
@Inject(DI.antennaNotesRepository)
|
||||||
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
@@ -76,24 +73,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
throw new ApiError(meta.errors.noSuchAntenna);
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteIdsRes = await this.redisClient.xrevrange(
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||||
`antennaTimeline:${antenna.id}`,
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
|
||||||
'-',
|
|
||||||
'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')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
@@ -104,14 +86,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||||
|
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
|
||||||
const notes = await query.getMany();
|
const notes = await query
|
||||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
.take(ps.limit)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
this.noteReadService.read(me.id, notes);
|
this.noteReadService.read(me.id, notes);
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -41,6 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
@@ -57,6 +58,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishUserEvent(me.id, 'followChannel', channel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,54 +0,0 @@
|
|||||||
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)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
throw new ApiError(meta.errors.noSuchChannel);
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.channelEntityService.pack(channel, me, true);
|
return await this.channelEntityService.pack(channel, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -50,16 +48,12 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
@@ -73,25 +67,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
throw new ApiError(meta.errors.noSuchChannel);
|
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
|
//#region Construct query
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
@@ -112,8 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const timeline = await query.getMany();
|
const timeline = await query.take(ps.limit).getMany();
|
||||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
|
||||||
|
|
||||||
if (me) this.activeUsersChart.read(me);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
|
@@ -1,56 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -38,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
@@ -52,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||||||
import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
|
import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
|
||||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
@@ -47,12 +47,6 @@ export const paramDef = {
|
|||||||
name: { type: 'string', minLength: 1, maxLength: 128 },
|
name: { type: 'string', minLength: 1, maxLength: 128 },
|
||||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
pinnedNoteIds: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string', format: 'misskey:id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: ['channelId'],
|
required: ['channelId'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -70,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
private channelEntityService: ChannelEntityService,
|
private channelEntityService: ChannelEntityService,
|
||||||
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
@@ -103,7 +97,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
await this.channelsRepository.update(channel.id, {
|
await this.channelsRepository.update(channel.id, {
|
||||||
...(ps.name !== undefined ? { name: ps.name } : {}),
|
...(ps.name !== undefined ? { name: ps.name } : {}),
|
||||||
...(ps.description !== undefined ? { description: ps.description } : {}),
|
...(ps.description !== undefined ? { description: ps.description } : {}),
|
||||||
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
|
|
||||||
...(banner ? { bannerId: banner.id } : {}),
|
...(banner ? { bannerId: banner.id } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -58,6 +58,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
category: 'ASC',
|
category: 'ASC',
|
||||||
name: 'ASC',
|
name: 'ASC',
|
||||||
},
|
},
|
||||||
|
cache: {
|
||||||
|
id: 'meta_emojis',
|
||||||
|
milliseconds: 3600000, // 1 hour
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Brackets, In } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js';
|
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
|
||||||
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
|
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
@@ -9,8 +8,6 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
|||||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { Notification } from '@/models/entities/Notification.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account', 'notifications'],
|
tags: ['account', 'notifications'],
|
||||||
@@ -41,6 +38,8 @@ export const paramDef = {
|
|||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
following: { type: 'boolean', default: false },
|
||||||
|
unreadOnly: { type: 'boolean', default: false },
|
||||||
markAsRead: { type: 'boolean', default: true },
|
markAsRead: { type: 'boolean', default: true },
|
||||||
// 後方互換のため、廃止された通知タイプも受け付ける
|
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||||
includeTypes: { type: 'array', items: {
|
includeTypes: { type: 'array', items: {
|
||||||
@@ -57,22 +56,21 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notificationsRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
|
||||||
private notificationEntityService: NotificationEntityService,
|
private notificationEntityService: NotificationEntityService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
@@ -91,39 +89,85 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
|
|
||||||
const notificationsRes = await this.redisClient.xrevrange(
|
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||||
`notificationTimeline:${me.id}`,
|
.select('following.followeeId')
|
||||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
.where('following.followerId = :followerId', { followerId: me.id });
|
||||||
'-',
|
|
||||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
|
||||||
|
|
||||||
if (notificationsRes.length === 0) {
|
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||||
return [];
|
.select('muting.muteeId')
|
||||||
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
|
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||||
|
.select('user_profile.mutedInstances')
|
||||||
|
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
|
const suspendedQuery = this.usersRepository.createQueryBuilder('users')
|
||||||
|
.select('users.id')
|
||||||
|
.where('users.isSuspended = TRUE');
|
||||||
|
|
||||||
|
const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere('notification.notifieeId = :meId', { meId: me.id })
|
||||||
|
.leftJoinAndSelect('notification.notifier', 'notifier')
|
||||||
|
.leftJoinAndSelect('notification.note', 'note')
|
||||||
|
.leftJoinAndSelect('notifier.avatar', 'notifierAvatar')
|
||||||
|
.leftJoinAndSelect('notifier.banner', 'notifierBanner')
|
||||||
|
.leftJoinAndSelect('note.user', 'user')
|
||||||
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
|
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||||
|
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||||
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
|
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||||
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
|
|
||||||
|
// muted users
|
||||||
|
query.andWhere(new Brackets(qb => { qb
|
||||||
|
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
|
||||||
|
.orWhere('notification.notifierId IS NULL');
|
||||||
|
}));
|
||||||
|
query.setParameters(mutingQuery.getParameters());
|
||||||
|
|
||||||
|
// muted instances
|
||||||
|
query.andWhere(new Brackets(qb => { qb
|
||||||
|
.andWhere('notifier.host IS NULL')
|
||||||
|
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
|
||||||
|
}));
|
||||||
|
query.setParameters(mutingInstanceQuery.getParameters());
|
||||||
|
|
||||||
|
// suspended users
|
||||||
|
query.andWhere(new Brackets(qb => { qb
|
||||||
|
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
|
||||||
|
.orWhere('notification.notifierId IS NULL');
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (ps.following) {
|
||||||
|
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id });
|
||||||
|
query.setParameters(followingQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
|
|
||||||
|
|
||||||
if (includeTypes && includeTypes.length > 0) {
|
if (includeTypes && includeTypes.length > 0) {
|
||||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
|
||||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notifications.length === 0) {
|
if (ps.unreadOnly) {
|
||||||
return [];
|
query.andWhere('notification.isRead = false');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifications = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
// Mark all as read
|
// Mark all as read
|
||||||
if (ps.markAsRead) {
|
if (notifications.length > 0 && ps.markAsRead) {
|
||||||
this.notificationService.readAllNotification(me.id);
|
this.notificationService.readNotification(me.id, notifications.map(x => x.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteIds = notifications
|
const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
|
||||||
.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type))
|
|
||||||
.map(notification => notification.noteId!);
|
|
||||||
|
|
||||||
if (noteIds.length > 0) {
|
if (notes.length > 0) {
|
||||||
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
|
|
||||||
this.noteReadService.read(me.id, notes);
|
this.noteReadService.read(me.id, notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id });
|
const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||||
const oldToken = freshUser.token!;
|
const oldToken = freshUser.token;
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||||
|
|
||||||
@@ -54,6 +54,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
// Publish event
|
// Publish event
|
||||||
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
||||||
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
|
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
|
||||||
|
|
||||||
|
// Terminate streaming
|
||||||
|
setTimeout(() => {
|
||||||
|
this.globalEventService.publishUserEvent(me.id, 'terminate', {});
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
id: ps.tokenId,
|
id: ps.tokenId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Terminate streaming
|
||||||
|
this.globalEventService.publishUserEvent(me.id, 'terminate');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user